MyBatis 基础认知、配置体系与核心映射

本文件覆盖 MyBatis 从入门到高级的基础能力:它解决什么问题、和 JDBC / JPA 的区别、核心运行流程、全局配置、Mapper 注册、参数绑定、基本 CRUD、最小 Demo,以及入门到专家的基础面试题答案。

官方参考:

1. MyBatis 解决什么问题

MyBatis 是一个半自动 ORM / SQL Mapper 框架。它不试图隐藏 SQL,而是让开发者显式编写 SQL,并负责:

  • 创建和管理 JDBC PreparedStatement
  • 把 Java 参数绑定到 SQL。
  • 执行 SQL。
  • ResultSet 映射为 Java 对象。
  • 管理一级缓存、二级缓存、插件拦截、TypeHandler 等机制。

传统 JDBC 代码:

java 复制代码
String sql = "select id, username, email from users where id = ?";
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
  ps.setLong(1, id);
  try (ResultSet rs = ps.executeQuery()) {
    if (rs.next()) {
      User user = new User();
      user.setId(rs.getLong("id"));
      user.setUsername(rs.getString("username"));
      user.setEmail(rs.getString("email"));
      return user;
    }
  }
}

MyBatis Mapper:

java 复制代码
public interface UserMapper {
  User selectById(Long id);
}
xml 复制代码
<select id="selectById" parameterType="long" resultType="com.example.User">
  select id, username, email
  from users
  where id = #{id}
</select>

MyBatis 的价值是减少 JDBC 样板代码,同时保留 SQL 控制权。

2. MyBatis、JDBC、JPA 的区别

维度 JDBC MyBatis JPA/Hibernate
SQL 控制 完全手写 手写 SQL 为主 框架生成 SQL 为主
映射能力 手写 ResultSet XML/注解映射 Entity 映射
学习成本 低但样板多 中等
复杂查询 可控但繁琐 强,可直接优化 SQL 复杂查询可能绕
性能可控性 依赖 ORM 策略
适用场景 小工具/底层 业务系统、复杂 SQL 领域模型驱动、CRUD 多

MyBatis 适合:

  • SQL 较复杂。
  • 报表、搜索、条件查询多。
  • 需要精确控制 SQL 性能。
  • 团队熟悉数据库。
  • 不希望 ORM 自动生成不可控 SQL。

不适合:

  • 完全不想写 SQL。
  • 领域模型关系复杂且希望 ORM 自动管理对象图。
  • 业务几乎都是简单 CRUD 且团队更偏 JPA。

3. MyBatis 核心运行流程

典型流程:

text 复制代码
Mapper 方法调用
  -> MapperProxy
  -> MappedStatement
  -> SqlSource 生成 BoundSql
  -> ParameterHandler 绑定参数
  -> StatementHandler 执行 SQL
  -> ResultSetHandler 处理结果集
  -> TypeHandler 做 Java/JDBC 类型转换

关键概念:

  • SqlSessionFactory:创建 SqlSession 的工厂。
  • SqlSession:执行 SQL、获取 Mapper、管理一级缓存的会话对象。
  • Mapper:Java 接口,方法与 SQL statement 对应。
  • MappedStatement:一条 SQL 语句的完整元数据。
  • BoundSql:动态 SQL 解析后的最终 SQL 和参数映射。
  • Executor:执行器,负责查询、更新、缓存等。

4. 最小配置 Demo:纯 MyBatis

目录:

text 复制代码
mybatis-demo/
├── pom.xml
├── src/main/java/com/example/User.java
├── src/main/java/com/example/UserMapper.java
├── src/main/java/com/example/Main.java
└── src/main/resources/
    ├── mybatis-config.xml
    └── mapper/UserMapper.xml

Maven 依赖:

xml 复制代码
<dependencies>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.16</version>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
  </dependency>
</dependencies>

实体:

java 复制代码
package com.example;

public class User {
  private Long id;
  private String username;
  private String email;

  public Long getId() { return id; }
  public void setId(Long id) { this.id = id; }

  public String getUsername() { return username; }
  public void setUsername(String username) { this.username = username; }

  public String getEmail() { return email; }
  public void setEmail(String email) { this.email = email; }
}

Mapper 接口:

java 复制代码
package com.example;

public interface UserMapper {
  User selectById(Long id);
  int insert(User user);
}

MyBatis 配置:

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
  </settings>

  <typeAliases>
    <typeAlias alias="User" type="com.example.User"/>
  </typeAliases>

  <environments default="dev">
    <environment id="dev">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
  </mappers>
</configuration>

Mapper XML:

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
  <select id="selectById" parameterType="long" resultType="User">
    select id, username, email
    from users
    where id = #{id}
  </select>

  <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    insert into users(username, email)
    values(#{username}, #{email})
  </insert>
</mapper>

启动代码:

java 复制代码
package com.example;

import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class Main {
  public static void main(String[] args) throws Exception {
    try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml")) {
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);

      try (SqlSession session = factory.openSession()) {
        session.update("org.apache.ibatis.jdbc.ScriptRunner"); // 示例中实际应先执行 schema.sql
      }

      try (SqlSession session = factory.openSession(true)) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = new User();
        user.setUsername("ada");
        user.setEmail("ada@example.com");
        mapper.insert(user);

        User loaded = mapper.selectById(user.getId());
        System.out.println(loaded.getUsername());
      }
    }
  }
}

说明:真实 Demo 中建议用 H2 初始化脚本或 Spring Boot 自动执行 schema.sql,这里重点展示 MyBatis 最小运行链路。

5. 全局配置结构

官方配置顶层结构包括:

xml 复制代码
<configuration>
  <properties/>
  <settings/>
  <typeAliases/>
  <typeHandlers/>
  <objectFactory/>
  <plugins/>
  <environments/>
  <databaseIdProvider/>
  <mappers/>
</configuration>

常用配置:

xml 复制代码
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="false"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="logImpl" value="SLF4J"/>
</settings>

关键含义:

  • mapUnderscoreToCamelCaseuser_name 自动映射到 userName
  • cacheEnabled:全局二级缓存开关。
  • lazyLoadingEnabled:延迟加载开关。
  • defaultExecutorType:默认执行器,SIMPLEREUSEBATCH
  • logImpl:日志实现。

6. Mapper XML 的核心元素

Mapper XML 常用元素:

  • cache:命名空间二级缓存。
  • cache-ref:引用其他 namespace 缓存。
  • resultMap:复杂结果映射。
  • sql:可复用 SQL 片段。
  • select:查询。
  • insert:插入。
  • update:更新。
  • delete:删除。

示例:

xml 复制代码
<mapper namespace="com.example.UserMapper">
  <sql id="BaseColumns">
    id, username, email, created_at
  </sql>

  <select id="selectAll" resultType="User">
    select <include refid="BaseColumns"/>
    from users
  </select>
</mapper>

7. #{}${} 的区别

#{} 使用 PreparedStatement 参数绑定,安全,防 SQL 注入:

xml 复制代码
where username = #{username}

${} 是字符串拼接,危险:

xml 复制代码
order by ${sort}

#{} 会生成:

sql 复制代码
where username = ?

${} 会直接拼接:

sql 复制代码
order by created_at desc

专家实践:业务值必须使用 #{}。只有列名、排序方向、表名等无法参数化的位置才考虑 ${},并必须做白名单校验。

白名单示例:

java 复制代码
public enum UserSortField {
  ID("id"),
  CREATED_AT("created_at");

  private final String column;

  UserSortField(String column) {
    this.column = column;
  }

  public String column() {
    return column;
  }
}

XML:

xml 复制代码
order by ${sortField.column} ${sortDirection}

其中 sortDirection 也必须由枚举或白名单生成。

8. 参数绑定

单参数:

java 复制代码
User selectById(Long id);
xml 复制代码
where id = #{id}

多参数建议使用 @Param

java 复制代码
List<User> selectByStatusAndKeyword(
  @Param("status") String status,
  @Param("keyword") String keyword
);
xml 复制代码
where status = #{status}
and username like concat('%', #{keyword}, '%')

对象参数:

java 复制代码
int insert(User user);
xml 复制代码
values(#{username}, #{email})

Map 参数可用但不推荐在复杂业务中过度使用,因为类型不安全。

9. CRUD 最小 Demo

Mapper:

java 复制代码
public interface ProductMapper {
  Product selectById(Long id);
  List<Product> selectAll();
  int insert(Product product);
  int update(Product product);
  int deleteById(Long id);
}

XML:

xml 复制代码
<mapper namespace="com.example.ProductMapper">
  <select id="selectById" resultType="Product">
    select id, name, price, stock
    from products
    where id = #{id}
  </select>

  <select id="selectAll" resultType="Product">
    select id, name, price, stock
    from products
    order by id desc
  </select>

  <insert id="insert" parameterType="Product" useGeneratedKeys="true" keyProperty="id">
    insert into products(name, price, stock)
    values(#{name}, #{price}, #{stock})
  </insert>

  <update id="update" parameterType="Product">
    update products
    set name = #{name},
        price = #{price},
        stock = #{stock}
    where id = #{id}
  </update>

  <delete id="deleteById">
    delete from products
    where id = #{id}
  </delete>
</mapper>

10. 注解 Mapper

简单 SQL 可用注解:

java 复制代码
public interface UserMapper {
  @Select("select id, username, email from users where id = #{id}")
  User selectById(Long id);

  @Insert("""
    insert into users(username, email)
    values(#{username}, #{email})
    """)
  @Options(useGeneratedKeys = true, keyProperty = "id")
  int insert(User user);
}

注解适合:

  • SQL 很短。
  • 查询不复杂。
  • Demo 或简单 CRUD。

XML 适合:

  • 动态 SQL 多。
  • ResultMap 复杂。
  • SQL 需要审查和调优。
  • 团队希望 SQL 与 Java 分离。

11. 一级缓存

一级缓存是 SqlSession 级别缓存,默认开启。

java 复制代码
try (SqlSession session = factory.openSession()) {
  UserMapper mapper = session.getMapper(UserMapper.class);
  User a = mapper.selectById(1L);
  User b = mapper.selectById(1L);
}

同一个 SqlSession 中,两次相同查询可能命中一级缓存。

注意:

  • insert/update/delete 会清理缓存。
  • 不同 SqlSession 不共享一级缓存。
  • Spring 集成中 SqlSession 通常由事务边界管理。

12. 入门到专家知识点清单

入门:

  • MyBatis 定位。
  • Mapper 接口。
  • Mapper XML。
  • #{} 参数绑定。
  • 基础 CRUD。
  • SqlSessionFactory / SqlSession

进阶:

  • 全局配置。
  • @Param
  • 注解 Mapper。
  • SQL 片段。
  • 一级缓存。
  • XML 与注解取舍。

高级:

  • MappedStatement
  • BoundSql
  • Executor
  • ParameterHandler
  • ResultSetHandler
  • TypeHandler。

精通:

  • Spring Boot 集成。
  • 事务边界。
  • 批量执行。
  • 插件机制。
  • 二级缓存。
  • SQL 性能调优。

专家:

  • Mapper 分层治理。
  • SQL 安全策略。
  • 多数据源。
  • 读写分离。
  • 分库分表边界。
  • 数据访问层架构设计。

13. 面试题与完整答案

13.1 MyBatis 是 ORM 吗?

MyBatis 通常被称为半自动 ORM 或 SQL Mapper。它不像 Hibernate 那样自动生成大部分 SQL,而是由开发者显式编写 SQL,MyBatis 负责参数绑定、执行、结果映射、缓存和扩展。它更强调 SQL 可控性。

13.2 MyBatis 和 JDBC 的区别是什么?

JDBC 需要手写连接、Statement、参数绑定、ResultSet 映射、异常处理和资源释放。MyBatis 封装了这些样板代码,让开发者专注 SQL 和映射关系。同时 MyBatis 提供 Mapper 接口、动态 SQL、ResultMap、缓存和插件机制。

13.3 #{}${} 的区别是什么?

#{} 会使用 PreparedStatement 参数占位符,安全防注入;${} 是字符串拼接,会直接进入 SQL。业务值必须用 #{},只有列名、排序方向等无法参数化的位置才可能使用 ${},并且必须做白名单校验。

13.4 为什么建议多参数使用 @Param

@Param 能给参数明确命名,使 XML 中可以用 #{status}#{keyword} 访问,避免依赖 MyBatis 默认参数名如 param1arg0,提高可读性和可维护性。

13.5 XML 和注解 Mapper 如何选择?

简单 SQL 可用注解,代码集中、阅读方便。复杂动态 SQL、复杂 ResultMap、SQL 需要审查或调优时优先 XML。大型项目通常 XML 更可维护,因为 SQL 可以独立组织和优化。

13.6 一级缓存是什么?

一级缓存是 SqlSession 级缓存,同一个 SqlSession 中相同查询可复用结果。它默认开启,insert/update/delete 会清理缓存。Spring 场景下一级缓存通常受事务和 SqlSession 生命周期影响。

13.7 MyBatis 的核心执行链路是什么?

Mapper 方法通过动态代理进入 MyBatis,找到对应 MappedStatement,由 SqlSource 生成 BoundSqlParameterHandler 绑定参数,StatementHandler 执行 SQL,ResultSetHandler 映射结果,TypeHandler 负责 Java/JDBC 类型转换。

13.8 MyBatis 最大的优势和风险是什么?

优势是 SQL 可控、性能可调、复杂查询表达能力强。风险是 SQL 分散、动态 SQL 复杂后难维护、${} 容易注入、N+1 查询、Mapper 边界混乱。专家级使用 MyBatis 的关键是建立数据访问层规范。

14. 配置项扩展矩阵

MyBatis 的全局配置对运行行为影响很大。以下配置在工程中最常见。

配置 作用 默认倾向 专家建议
mapUnderscoreToCamelCase 下划线转驼峰 false Java 项目常开启
cacheEnabled 二级缓存总开关 true 开启不代表所有 Mapper 都缓存
lazyLoadingEnabled 延迟加载 false 谨慎开启,避免隐藏 N+1
aggressiveLazyLoading 激进延迟加载 false 通常保持 false
defaultExecutorType 执行器类型 SIMPLE 批处理单独设计
defaultStatementTimeout SQL 超时时间 unset 生产建议设置
defaultFetchSize JDBC fetch size unset 大结果集可设置
safeRowBoundsEnabled 嵌套语句 RowBounds 安全 false 不推荐依赖 RowBounds 分页
localCacheScope 一级缓存范围 SESSION 高一致性场景可改 STATEMENT
jdbcTypeForNull null 参数 JDBC 类型 OTHER Oracle 等场景常需配置
callSettersOnNulls null 也调用 setter false Map 或特殊对象需要时开启
returnInstanceForEmptyRow 空行返回空对象 false 嵌套映射场景谨慎开启
logImpl 日志实现 自动发现 生产用 SLF4J

示例:

xml 复制代码
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
  <setting name="defaultStatementTimeout" value="5"/>
  <setting name="defaultFetchSize" value="200"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="logImpl" value="SLF4J"/>
</settings>

15. Mapper 方法与 Statement 匹配规则

Mapper 接口:

java 复制代码
package com.example.mapper;

public interface UserMapper {
  User selectById(Long id);
}

XML:

xml 复制代码
<mapper namespace="com.example.mapper.UserMapper">
  <select id="selectById" resultType="User">
    select id, username from users where id = #{id}
  </select>
</mapper>

匹配关系:

text 复制代码
namespace + "." + statement id
= com.example.mapper.UserMapper.selectById

常见错误:

  • namespace 写错。
  • XML 文件未被扫描。
  • 方法名和 statement id 不一致。
  • Mapper 接口包名变了但 XML 没改。
  • 重载方法导致 statement id 冲突。

专家建议:Mapper 方法不要重载。MyBatis statement id 只按方法名匹配,重载会造成语义混乱。

16. 参数命名深入

@Param 的多参数方法:

java 复制代码
User select(String username, String email);

MyBatis 可能提供:

text 复制代码
arg0, arg1, param1, param2

不推荐在 XML 中依赖这些名字。

推荐:

java 复制代码
User select(@Param("username") String username, @Param("email") String email);

XML:

xml 复制代码
where username = #{username}
and email = #{email}

集合参数:

java 复制代码
List<User> selectByIds(@Param("ids") List<Long> ids);

XML:

xml 复制代码
<foreach collection="ids" item="id" open="(" separator="," close=")">
  #{id}
</foreach>

17. 主键回填详解

自增主键:

xml 复制代码
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
  insert into users(username, email)
  values(#{username}, #{email})
</insert>

插入后:

java 复制代码
User user = new User();
user.setUsername("ada");
mapper.insert(user);
Long id = user.getId();

非自增主键可在业务层生成:

java 复制代码
user.setId(idGenerator.nextId());
mapper.insert(user);

专家建议:

  • 分布式系统优先使用雪花 ID、号段模式或数据库序列。
  • 自增 ID 简单,但分库分表和数据迁移时会受限。
  • 主键生成策略属于架构决策,不只是 Mapper 配置。

18. Java 类型与 JDBC 类型

参数可指定 JDBC 类型:

xml 复制代码
where deleted_at is #{deletedAt,jdbcType=TIMESTAMP}

null 值场景有些数据库需要明确 JDBC 类型:

xml 复制代码
#{remark,jdbcType=VARCHAR}

常见映射:

Java JDBC
String VARCHAR
Long BIGINT
Integer INTEGER
BigDecimal DECIMAL
LocalDateTime TIMESTAMP
Boolean BOOLEAN / BIT
Enum VARCHAR / INTEGER

19. 最小 Debug Demo:查看最终 SQL

拦截器或日志可查看 BoundSql。

java 复制代码
MappedStatement ms = configuration.getMappedStatement(
  "com.example.UserMapper.selectById"
);
BoundSql boundSql = ms.getBoundSql(1L);
System.out.println(boundSql.getSql());
System.out.println(boundSql.getParameterMappings());

理解 BoundSql 有助于排查:

  • 动态 SQL 最终生成什么。
  • 参数名是否正确。
  • foreach 是否展开。
  • include 是否生效。

20. 基础反模式扩展

20.1 Mapper 返回 Map

java 复制代码
Map<String, Object> selectUser(Long id);

问题:

  • 类型不安全。
  • 字段名散落业务层。
  • 重构困难。
  • IDE 无法帮助检查。

只在通用报表、临时工具或字段不固定场景使用。

20.2 select *

风险:

  • 表结构变化影响对象映射。
  • 多取无用列。
  • 覆盖索引失效。
  • join 时列名冲突。

推荐明确列名。

20.3 Mapper 方法语义不清

不推荐:

java 复制代码
List<User> query(Map<String, Object> params);

推荐:

java 复制代码
List<User> searchActiveUsers(UserSearchQuery query);

21. 基础阶段专家题补充

21.1 为什么 MyBatis 更适合 SQL 能力强的团队?

MyBatis 把 SQL 控制权交给开发者,复杂查询、索引、分页、锁和执行计划都需要团队理解数据库。如果团队 SQL 能力弱,可能写出大量慢 SQL、N+1 查询和注入风险。MyBatis 的自由度越高,对规范和审查要求越高。

21.2 为什么不要让数据库表结构直接决定领域模型?

数据库表是持久化结构,领域模型表达业务规则。两者生命周期不同。表结构可能为了性能、历史兼容或报表做妥协,领域模型应表达业务概念。复杂系统中应通过 Repository 或 Assembler 做转换。

21.3 MyBatis 是否会自动管理对象关系?

不会像 Hibernate 那样自动管理持久化上下文和对象关系。MyBatis 可以通过 ResultMap 映射 association/collection,但查询时机和 SQL 仍由开发者设计。这让性能更可控,也要求开发者主动处理对象图加载策略。

相关推荐
落木萧萧82521 小时前
为什么我把 MyBatisGX 设计成现在这样
mybatis·orm
代码旅人ing21 小时前
Redis+Spring+MyBatis + 微服务 + 消息队列核心知识点(面试高频题目合集)
redis·spring·mybatis·java-rabbitmq
Devin~Y21 小时前
大厂Java面试实录:Spring Boot/Cloud、Kafka、Redis、K8s 可观测性 + RAG/Agent(小Y翻车版)
java·spring boot·redis·spring cloud·kafka·kubernetes·mybatis
ppandss11 天前
JavaWeb从0到1-DAY11-MyBatis入门
java·tomcat·mybatis
JAVA面经实录9171 天前
MyBatis面试题库
java·mybatis
杨运交1 天前
[022][数据模块]基于雪花算法的 MyBatis-Plus 主键生成器设计与实现
mybatis
Mahir082 天前
MyBatis 深度解密:从执行流程到底层原理全解
java·后端·面试·mybatis
Mahir082 天前
MyBatis 分页与插件深度解密:从插件机制到三大分页方案原理全解
java·后端·mybatis·mybatis-plus·大厂面试题
谷哥的小弟2 天前
图文详解Spring Boot整合MyBatisPlus(附源码)
mybatis·源码·springboot·mybatis-plus·整合
醉颜凉2 天前
Lucene底层原理:倒排索引实现原理与代码实战,彻底吃透搜索引擎核心
搜索引擎·mybatis·lucene