Skip to content

MyBatis 系统学习文档 ​

资料基准:本文以 MyBatis 3.5.19、MyBatis-Spring 4.0.0、MyBatis Spring Boot Starter 4.0.0 官方文档为主要依据整理。适合 Java 后端、Spring Boot 开发者从入门到进阶系统学习。

0. 学习目标与路线 ​

0.1 你应该先理解什么 ​

MyBatis 不是一个“替你生成一切 SQL 的 ORM”,而是一个以 SQL 为中心的数据持久层框架。它帮你完成 JDBC 中最繁琐的部分:连接会话、参数绑定、结果映射、事务配合、缓存、动态 SQL、插件扩展等。

学习顺序建议:

  1. 先理解 JDBC、PreparedStatement、事务、连接池、SQL 基础。
  2. 再掌握 MyBatis 的四个核心对象:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Mapper。
  3. 继续学习 XML Mapper、参数绑定、结果映射、动态 SQL。
  4. 进入缓存、批处理、分页、插件、类型处理器、Spring 集成。
  5. 最后学习性能分析、复杂映射、读写一致性、项目规范与常见坑。

0.2 分层学习地图 ​

阶段目标必学知识
基础能写简单增删改查配置文件、Mapper 接口、XML 映射、#{}、resultType
进阶能处理真实业务查询resultMap、动态 SQL、分页、主键回填、多参数、事务
高级能做稳定工程落地一级/二级缓存、批处理、插件、类型处理器、复杂关联、性能优化
工程化能在团队项目中维护Spring Boot 自动配置、Mapper 扫描、日志、测试、规范、故障定位

1. MyBatis 的定位 ​

1.1 JDBC 的痛点 ​

直接使用 JDBC 时,常见重复劳动包括:

  • 手动获取和释放连接。
  • 手动创建 PreparedStatement。
  • 手动设置 SQL 参数。
  • 手动遍历 ResultSet 并转换为 Java 对象。
  • 手动处理事务、异常、资源关闭。

MyBatis 的价值是把这些模板代码抽象掉,但仍然保留开发者对 SQL 的控制权。

1.2 MyBatis 与 ORM 的区别 ​

对比项MyBatis典型全自动 ORM
SQL 控制开发者手写 SQL,控制强框架生成 SQL 为主
学习重点SQL 映射、动态 SQL、结果映射对象关系、实体状态、级联
适用场景复杂查询、报表、强 SQL 优化需求标准 CRUD、领域模型驱动
风险SQL 分散、动态 SQL 滥用生成 SQL 不透明、N+1 隐蔽

1.3 MyBatis 适合什么项目 ​

适合:

  • 业务 SQL 较复杂,需要精细优化。
  • 团队熟悉 SQL,希望可控、可审计。
  • 系统中存在大量报表、多表查询、历史库查询。
  • 需要和存量数据库、复杂命名规则、存储过程集成。

不适合:

  • 完全不希望写 SQL。
  • 领域模型生命周期非常复杂,并希望 ORM 管理实体状态。
  • 团队缺少 SQL 基础,又缺乏数据库审查机制。

2. 核心对象与执行流程 ​

2.1 四个核心对象 ​

对象作用生命周期建议
SqlSessionFactoryBuilder读取配置并构建 SqlSessionFactory构建完即可丢弃
SqlSessionFactory创建 SqlSession,通常对应一个数据库环境应用级单例
SqlSession执行 SQL、获取 Mapper、提交/回滚事务请求级或方法级,不能跨线程共享
Mapper 接口业务代码调用的数据访问接口由 MyBatis 或 Spring 代理生成

2.2 一次查询的典型流程 ​

  1. 应用启动时读取 mybatis-config.xml。
  2. 解析全局配置、环境、Mapper XML、Mapper 接口。
  3. 创建 SqlSessionFactory。
  4. 调用时打开 SqlSession。
  5. 通过 Mapper 代理调用接口方法。
  6. MyBatis 根据 namespace + id 找到 mapped statement。
  7. 解析动态 SQL,生成最终 SQL 与参数映射。
  8. 使用 JDBC PreparedStatement 执行。
  9. 将 ResultSet 映射为 Java 对象。
  10. 按事务策略提交、回滚、关闭会话。

2.3 Mapper 方法与 XML 的绑定规则 ​

Mapper XML 中的 namespace 通常等于 Mapper 接口全限定名,语句 id 通常等于接口方法名。

xml
<mapper namespace="com.example.user.UserMapper">
  <select id="findById" parameterType="long" resultType="com.example.user.User">
    select id, username, email
    from users
    where id = #{id}
  </select>
</mapper>
java
public interface UserMapper {
    User findById(Long id);
}

只要命名空间和方法名匹配,调用 userMapper.findById(1L) 时就能找到对应 SQL。

2.4 内部执行链路:一次调用背后的核心对象 ​

把 2.2 的流程展开到对象级别(对应 mybatis.org 官方文档的架构描述),面试和排错都用得上:

text
userMapper.findById(1L)
  │
  ▼
MapperProxy(JDK 动态代理)          ← Mapper 接口没有实现类,代理把方法调用转成
  │                                    sqlSession.selectOne("namespace.id", args)
  ▼
MappedStatement                     ← 启动时解析 XML/注解得到的"语句对象":
  │                                    SQL 原文、SqlSource、parameterMappings、
  │                                    resultMap、timeout/fetchSize/flushCache/useCache 全在这
  ▼
CachingExecutor(装饰器)→ Simple/Reuse/Batch Executor   ← 二级缓存在 CachingExecutor 层,
  │                                    真正的 JDBC 执行在内部委托的 Executor
  ▼
StatementHandler → ParameterHandler → java.sql.PreparedStatement
  │                                    动态 SQL 在这里由 SqlSource.getBoundSql() 生成
  ▼
ResultSetHandler → TypeHandler → 目标 Java 对象 / resultMap 装配
内部对象职责排错时的意义
Configuration保存所有 MappedStatement、拦截器、TypeHandler、设置项Invalid bound statement 说明它里面没有这条语句
SqlSource / BoundSql静态 SQL / 运行时解析出的最终 SQL + ? 参数映射日志里打印的是 BoundSql,动态条件不符要回到 SqlNode 树排查
Executor一级缓存、批处理、事务边界一级/二级缓存、BATCH 行为都挂在它身上
TypeHandler#{} 参数写入与结果列读取的双向转换null 参数报驱动错误、枚举/JSON 映射失败都找它

💡 理解这条链后,13 章的插件机制就不再神秘:所谓"可拦截",就是这条链上四个对象暴露了 public 方法。

3. 快速入门 ​

3.1 Maven 依赖 ​

普通 Java 项目可使用 MyBatis 核心依赖:

xml
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.19</version>
</dependency>

Spring Boot 项目通常使用 Starter:

xml
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>4.0.0</version>
</dependency>

版本要和你的 Spring Boot 主版本匹配,不要只追最新。

3.2 最小配置文件 ​

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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/demo"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
  </mappers>
</configuration>

3.3 实体类 ​

java
public class User {
    private Long id;
    private String username;
    private String email;
    // getter / setter
}

3.4 Mapper 接口 ​

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

3.5 Mapper XML ​

xml
<mapper namespace="com.example.mapper.UserMapper">
  <select id="findById" resultType="com.example.domain.User">
    select id, username, email
    from users
    where id = #{id}
  </select>

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

3.6 原生 MyBatis 调用 ​

java
try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    User user = mapper.findById(1L);
    session.commit();
}

在 Spring Boot 中,通常不手动打开 SqlSession,而是注入 Mapper。

java
@Service
public class UserService {
    private final UserMapper userMapper;

    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public User getUser(Long id) {
        return userMapper.findById(id);
    }
}

4. 全局配置 ​

4.1 配置文件结构 ​

MyBatis 全局配置常见元素:

  • properties:外部属性,如数据库连接信息。
  • settings:全局行为设置。
  • typeAliases:类型别名。
  • typeHandlers:类型处理器。
  • objectFactory:对象工厂。
  • plugins:拦截器插件。
  • environments:数据源和事务环境。
  • databaseIdProvider:数据库厂商识别。
  • mappers:Mapper 注册。

4.2 常用 settings ​

设置作用常见建议
mapUnderscoreToCamelCase下划线列名映射为驼峰属性Java 项目常设为 true
cacheEnabled全局启用/禁用二级缓存谨慎开启,先理解失效规则
lazyLoadingEnabled是否启用懒加载复杂关联可用,但要防 N+1
aggressiveLazyLoading是否激进触发懒加载多数场景保持默认或谨慎
localCacheScope一级缓存作用域,SESSION 或 STATEMENT默认适合多数场景,强一致查询可考虑 STATEMENT
defaultExecutorType默认执行器,SIMPLE、REUSE、BATCH批量写入可局部使用 BATCH
logImpl日志实现配合 SLF4J/Logback 查看 SQL

4.3 typeAliases ​

类型别名能减少 XML 中的全限定类名。

xml
<typeAliases>
  <package name="com.example.domain"/>
</typeAliases>

配置后可以写:

xml
<select id="findById" resultType="User">
  select * from users where id = #{id}
</select>

建议:

  • 项目小可以使用包扫描。
  • 大项目要避免不同包下同名类造成歧义。
  • 公共实体、DTO、VO 要有清晰命名。

4.4 environments ​

environment 定义事务管理器和数据源。

常见事务管理器:

  • JDBC:MyBatis 自己使用 JDBC commit/rollback。
  • MANAGED:事务由容器管理,MyBatis 不直接提交或回滚。

在 Spring 项目中通常由 Spring 接管数据源和事务,不再使用原生 environments 管理生产连接。

5. Mapper XML 基础 ​

5.1 select ​

xml
<select id="findActiveUsers" resultType="User">
  select id, username, email, status
  from users
  where status = 'ACTIVE'
</select>

核心属性:

  • id:当前 namespace 下唯一。
  • parameterType:入参类型,多数情况下可省略。
  • resultType:直接映射到简单类型、Map、Java Bean。
  • resultMap:引用复杂映射规则。
  • timeout:查询超时秒数。
  • fetchSize:给 JDBC 驱动的抓取大小提示。
  • useCache:是否使用二级缓存。
  • flushCache:执行语句时是否刷新缓存。

resultType 和 resultMap 二选一,不要同时使用。

select 还有几个不常用但很重要的属性(官方文档全量):

属性默认值说明
statementTypePREPARED可选 STATEMENT(不预编译)、PREPARED、CALLABLE(存储过程)
resultSetType驱动决定FORWARD_ONLY / SCROLL_SENSITIVE / SCROLL_INSENSITIVE;流式读取时配合 fetchSize 使用
resultOrderedfalse仅对嵌套 select 有意义:声明结果已按主行分组排列,让 MyBatis 提前释放主行引用,大幅降低大结果集嵌套映射的内存占用
databaseId无配合 databaseIdProvider,同名语句可按数据库厂商提供不同 SQL
affectDatafalse3.5.12+,返回数据的 DML 语句需设为 true 以便事务正确控制

5.2 insert / update / delete ​

xml
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
  insert into users(username, email)
  values(#{username}, #{email})
</insert>
xml
<update id="updateEmail">
  update users
  set email = #{email}
  where id = #{id}
</update>
xml
<delete id="deleteById">
  delete from users where id = #{id}
</delete>

修改语句默认会刷新本地缓存和二级缓存。返回值通常是影响行数。

5.3 主键回填 ​

自增主键:

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

使用序列或自定义主键:

xml
<insert id="insertOrder">
  <selectKey keyProperty="id" resultType="long" order="BEFORE">
    select nextval('order_seq')
  </selectKey>
  insert into orders(id, user_id, amount)
  values(#{id}, #{userId}, #{amount})
</insert>

5.4 SQL 片段 ​

xml
<sql id="BaseColumns">
  id, username, email, status, created_at
</sql>

<select id="findById" resultMap="UserResultMap">
  select <include refid="BaseColumns"/>
  from users
  where id = #{id}
</select>

建议只复用稳定列清单、基础过滤条件,不要把过多业务逻辑塞进 SQL 片段,否则可读性会下降。

6. 参数绑定 ​

6.1 #{} 与 ${} 的区别 ​

写法本质安全性适合场景
#{name}使用 JDBC 占位参数安全,推荐值条件、插入、更新
${column}字符串拼接进 SQL有注入风险动态表名、列名、排序字段,必须白名单

内部机制:#{...} 在语句解析期生成一个 ParameterMapping,运行时由 ParameterHandler 按序调用 ps.setObject(i, value) 填进 ? 占位符——值永远不进 SQL 字符串,所以预编译缓存可复用、注入天然免疫。${...} 则在 SqlSource 生成 BoundSql 阶段就做了字符串替换,等价于手写拼接,数据库看到的最终 SQL 里已含用户输入。

#{} 还支持参数属性与 TypeHandler 修饰,常用形式:

xml
#{user.name}                        <!-- 嵌套属性,OGNL 取值 -->
#{email,jdbcType=VARCHAR}           <!-- 指定 JDBC 类型 -->
#{status,typeHandler=com.example.UserStatusTypeHandler}  <!-- 指定处理器 -->
#{amount,javaType=long,jdbcType=DECIMAL}

推荐写法:

xml
where username = #{username}

高风险写法:

xml
order by ${sortColumn}

如果必须动态排序,应在 Java 层把外部输入映射到白名单枚举。

java
enum UserSort {
    CREATED_AT("created_at"),
    USERNAME("username");

    private final String column;
}

6.2 单参数 ​

java
User findById(Long id);
xml
where id = #{id}

对于简单类型,XML 中参数名在多数情况下不重要,但统一命名更清晰。

6.3 多参数 ​

推荐使用 @Param。

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

不推荐依赖 param1、param2,可读性差且重构风险高。

6.4 对象参数 ​

java
int updateProfile(User user);
xml
update users
set username = #{username},
    email = #{email}
where id = #{id}

MyBatis 会通过 Java Bean 属性读取值。

6.5 Map 参数 ​

java
List<User> findByCondition(Map<String, Object> params);

Map 灵活但弱类型,适合临时查询,不适合作为核心接口长期使用。

6.6 null 与 jdbcType ​

当参数可能为 null,某些 JDBC 驱动需要明确 jdbcType。

xml
#{email,jdbcType=VARCHAR}

经验:

  • 插入或更新可空字段时,建议显式声明 jdbcType。
  • 类型转换复杂时使用自定义 TypeHandler。

7. 结果映射 ​

7.1 resultType ​

列名与属性名一致或可通过驼峰映射时,可以直接使用 resultType。

xml
<select id="findById" resultType="User">
  select id, username, email
  from users
  where id = #{id}
</select>

如果数据库列是 user_name,Java 属性是 userName,可开启:

xml
<setting name="mapUnderscoreToCamelCase" value="true"/>

7.2 resultMap ​

复杂映射应使用 resultMap。

xml
<resultMap id="UserResultMap" type="User">
  <id property="id" column="id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
  <result property="createdAt" column="created_at"/>
</resultMap>

id 标签非常重要,它帮助 MyBatis 识别对象身份,减少关联映射时的重复对象。

7.3 association:一对一 ​

xml
<resultMap id="OrderResultMap" type="Order">
  <id property="id" column="order_id"/>
  <result property="amount" column="amount"/>
  <association property="user" javaType="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
  </association>
</resultMap>

适合订单携带用户信息这类嵌套对象。

7.4 collection:一对多 ​

xml
<resultMap id="UserWithOrdersMap" type="User">
  <id property="id" column="user_id"/>
  <result property="username" column="username"/>
  <collection property="orders" ofType="Order">
    <id property="id" column="order_id"/>
    <result property="amount" column="amount"/>
  </collection>
</resultMap>

注意:

  • 一对多 join 会导致主表字段重复。
  • 必须设置主对象和子对象的 id 映射。
  • 分页时不要直接对 join 后的结果盲目分页,容易丢数据或重复数据。

7.5 嵌套 select 与 N+1 ​

xml
<association property="user"
             column="user_id"
             select="com.example.UserMapper.findById"/>

这种方式写起来清晰,但可能产生 N+1 查询。比如查 100 个订单,每个订单再查一次用户,总共 101 次 SQL。

解决思路:

  • 小数据量、低频场景可接受。
  • 高频列表页优先使用 join 一次查出。
  • 需要延迟加载时监控 SQL 数量。
  • 对关联对象做缓存时要明确一致性风险。

7.6 constructor 映射 ​

不可变对象或构造器注入可使用 constructor。

xml
<resultMap id="UserDtoMap" type="UserDto">
  <constructor>
    <idArg column="id" javaType="long"/>
    <arg column="username" javaType="string"/>
  </constructor>
</resultMap>

适合 DTO、值对象、不可变类。

8. 动态 SQL ​

8.1 if ​

xml
<select id="search" resultType="User">
  select id, username, email, status
  from users
  where 1 = 1
  <if test="status != null">
    and status = #{status}
  </if>
  <if test="keyword != null and keyword != ''">
    and username like concat('%', #{keyword}, '%')
  </if>
</select>

8.2 where ​

where 会自动处理多余的 and / or。

xml
<select id="search" resultType="User">
  select * from users
  <where>
    <if test="status != null">status = #{status}</if>
    <if test="keyword != null">and username like concat('%', #{keyword}, '%')</if>
  </where>
</select>

8.3 choose / when / otherwise ​

xml
<choose>
  <when test="id != null">
    id = #{id}
  </when>
  <when test="email != null">
    email = #{email}
  </when>
  <otherwise>
    status = 'ACTIVE'
  </otherwise>
</choose>

类似 Java 中的 switch 或多分支优先选择。

8.4 set ​

xml
<update id="updateSelective">
  update users
  <set>
    <if test="username != null">username = #{username},</if>
    <if test="email != null">email = #{email},</if>
    <if test="status != null">status = #{status},</if>
  </set>
  where id = #{id}
</update>

set 会处理末尾多余逗号。

8.5 foreach ​

批量查询:

xml
<select id="findByIds" resultType="User">
  select * from users
  where id in
  <foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

批量插入:

xml
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
  insert into users(username, email)
  values
  <foreach collection="list" item="user" separator=",">
    (#{user.username}, #{user.email})
  </foreach>
</insert>

注意:

  • IN 列表过长会影响 SQL 长度和执行计划。
  • 大批量写入要分批,每批几百到几千取决于数据库和字段量。
  • MySQL 多值插入与 MyBatis ExecutorType.BATCH 是两种不同优化方式。

8.6 bind ​

xml
<bind name="pattern" value="'%' + keyword + '%'"/>
and username like #{pattern}

bind 可减少重复表达式,但不要把复杂业务逻辑放进 XML。

8.7 trim ​

xml
<trim prefix="where" prefixOverrides="and |or ">
  <if test="status != null">and status = #{status}</if>
  <if test="email != null">and email = #{email}</if>
</trim>

where 和 set 本质上是常用 trim 场景的封装。

8.8 动态 SQL 的内部机制 ​

  • 每条含动态标签的语句在启动期被解析成 SqlNode 树(IfSqlNode、ForEachSqlNode、TextSqlNode、MixedSqlNode…),而不是提前生成 SQL 字符串。
  • 每次执行时才调用 DynamicSqlSource.getBoundSql(param):自顶向下遍历 SqlNode 树,用 OGNL 求值 test 表达式决定分支取舍,拼出最终 SQL 文本与 #{} 对应的 ParameterMapping 列表。
  • 因此同一个 Mapper 方法在不同参数下会生成不同 SQL——预编译语句缓存可能多版本并存;高频接口用结构差异过大的动态 SQL 会降低计划缓存命中率。
  • ⚠️ TextSqlNode 阶段才做 ${} 替换,而 #{} 是先生成占位符、后绑定值——这就是 6.1 中两者安全性差异的根源。
  • 💡 test 表达式对字符串比较使用 OGNL 语义:test="name != null and name != ''" 是标准写法;单字符字符串(如 "Y")在 OGNL 里可能被当作 char 比较,稳妥写法是 'Y'.toString() 或反过来比较 name == 'Y'.toString()。

9. 注解 Mapper ​

9.1 基础注解 ​

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

    @Update("update users set email = #{email} where id = #{id}")
    int updateEmail(@Param("id") Long id, @Param("email") String email);
}

9.2 XML 与注解如何选择 ​

场景建议
简单单表查询注解可接受
动态 SQL 较多XML 更清晰
复杂 resultMapXML 更适合
团队希望 SQL 集中审查XML 更容易统一管理
SQL 极短且和接口强绑定注解更轻量

实践建议:核心业务复杂 SQL 用 XML,简单辅助查询可用注解。

10. 缓存机制 ​

10.1 一级缓存 ​

一级缓存是 SqlSession 级别的本地缓存。默认在同一个会话中,相同语句和参数可能复用查询结果。

特点:

  • 默认启用。
  • 作用域默认是 SESSION。
  • 执行增删改、提交、回滚、关闭会话时会清理。
  • 不能跨 SqlSession 共享。

内部机制(官方 Java API 文档):

  • 缓存粒度是 语句 + 参数 + rowBounds + resultMap 全部相同才命中;只要有一条不满足就重新查询。
  • 它永远不能完全关闭:一级缓存还承担解析循环引用和加速嵌套查询的职责;能做的只是把 localCacheScope 设为 STATEMENT,把作用域缩小到"单条语句执行期间"。
  • 缓存存的是对象引用而不是副本:SESSION 作用域下,MyBatis 把同一对象实例返回给本会话内所有相同查询的调用方。

风险:

  • 长会话中可能读到旧值。
  • 和外部更新、手动 JDBC 更新混用时要谨慎。
  • 在 Spring 项目中会话通常绑定到事务边界,风险相对可控。
  • ⚠️ 最隐蔽的一条:拿到查询结果后不要修改对象(包括往返回的 List 里加删元素)——你的修改会留在缓存里,同会话后续"查库"其实拿到的是被你污染过的对象。官方最佳实践明确:不要改动 MyBatis 返回的对象;需要可变的就 select 成 DTO 或手动拷贝。

💡 Spring 集成下"一级缓存基本失效"的原因:默认每个 Mapper 调用若不在事务中,SqlSessionTemplate 会为每次操作打开并关闭独立的 SqlSession——会话都没了,会话级缓存自然不复用。想利用它,把多次相同查询包进同一个 @Transactional 方法。

10.2 二级缓存 ​

二级缓存是 Mapper namespace 级别缓存,需要配置。

xml
<mapper namespace="com.example.UserMapper">
  <cache/>
</mapper>

<cache/> 一行配置背后是官方文档定义的默认行为,可调属性:

属性默认值说明
evictionLRU可选 LRU / FIFO / SOFT / WEAK
flushInterval未设置单位毫秒;不设置则只被语句调用刷新
size1024缓存的对象引用/列表条数,注意对象大小与环境内存
readOnlyfalsetrue 返回同一实例(快但调用方不能改);false 返回序列化副本(安全但慢)

内部机制与陷阱:

  • 缓存与 namespace 绑定:同 namespace 的所有语句共享一份缓存;select 默认 useCache="true"、flushCache="false",insert/update/delete 默认 flushCache="true"(执行即清空本 namespace 缓存——粒度是命名空间级,不是行级/键级,改一行也会让该 namespace 全部查询缓存作废)。
  • 二级缓存是事务性的:结果只在 SqlSession commit 后才进入二级缓存;未提交会话读不到别人还没提交的数据。
  • ⚠️ 只缓存本 namespace 的写操作:如果 OrderMapper 的更新影响了两者的 join 结果,UserMapper 的缓存不会被清——跨 namespace/跨表的 join 查询是二级缓存事故高发区,必须用 <cache-ref> 或应用层失效策略显式处理。
  • ⚠️ 缓存对象需可序列化(readOnly="false" 时按序列化拷贝)。
  • 使用 Java 接口注解声明的语句默认不会走 XML 里的 <cache/>,需要用 @CacheNamespaceRef 显式引用。

select 默认 useCache=true,insert/update/delete 默认刷新缓存。可按语句调整:

xml
<select id="findById" useCache="false" resultType="User">
  select * from users where id = #{id}
</select>

二级缓存适合:

  • 读多写少。
  • 数据一致性要求不苛刻。
  • 查询结果可序列化或缓存实现支持。
  • 缓存粒度和失效策略清晰。

不适合:

  • 高频更新表。
  • 多系统同时写同一张表。
  • 复杂 join 查询跨多个 namespace。
  • 强一致读场景。

10.3 cache-ref ​

不同 Mapper 可共享同一个缓存命名空间:

xml
<cache-ref namespace="com.example.UserMapper"/>

共享缓存会增加耦合,只有在边界非常明确时使用。

11. 事务与 Spring 集成 ​

11.1 原生 MyBatis 事务 ​

java
try (SqlSession session = sqlSessionFactory.openSession(false)) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    mapper.insert(user);
    session.commit();
} catch (Exception e) {
    session.rollback();
    throw e;
}

手动模式下要明确:

  • openSession(false) 表示关闭自动提交。
  • 成功调用 commit()。
  • 异常调用 rollback()。
  • 最后关闭 SqlSession。

11.2 MyBatis-Spring ​

MyBatis-Spring 使用 SqlSessionTemplate 管理会话。它是线程安全的,可以被多个 DAO 或 Mapper 共享,并会和 Spring 当前事务关联。

在 Spring 中推荐:

java
@Service
public class OrderService {
    private final OrderMapper orderMapper;

    public OrderService(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

不要在 Spring 管理的 SqlSession 上手动调用 commit()、rollback()、close()。

11.3 Mapper 扫描 ​

java
@SpringBootApplication
@MapperScan("com.example.mapper")
public class DemoApplication {
}

也可以在接口上使用 @Mapper。

实践建议:

  • 中大型项目使用 @MapperScan 统一扫描。
  • 多数据源项目要清楚指定 SqlSessionFactory 或 SqlSessionTemplate。
  • Mapper 接口不要写业务逻辑。

11.4 Spring Boot 常用配置 ​

yaml
mybatis:
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: com.example.domain
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

常见问题:

  • XML 没有打包到 classpath。
  • namespace 和接口全限定名不一致。
  • 方法名和 SQL id 不一致。
  • 多数据源下扫描到了错误的 Mapper。

12. 类型处理器 TypeHandler ​

12.1 TypeHandler 的作用 ​

TypeHandler 负责 Java 类型和 JDBC 类型之间的转换。

常见场景:

  • 枚举与字符串/数字映射。
  • JSON 字段与对象映射。
  • 加密字段读写转换。
  • 自定义日期时间格式。
  • 数据库特殊类型,如 PostgreSQL JSONB、数组。

12.2 自定义枚举处理器 ​

java
public enum UserStatus {
    ACTIVE, DISABLED
}
java
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                    UserStatus parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter.name());
    }

    @Override
    public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return value == null ? null : UserStatus.valueOf(value);
    }
}

注册:

xml
<typeHandlers>
  <typeHandler handler="com.example.mybatis.UserStatusTypeHandler"/>
</typeHandlers>

12.3 使用建议 ​

  • 类型转换规则统一放 TypeHandler,不要在每条 SQL 中手动转换。
  • JSON 字段要明确序列化库、字段兼容策略和异常处理。
  • 枚举入库值要稳定,避免改名导致历史数据不可读。

13. 插件机制 ​

13.1 插件能拦截什么 ​

MyBatis 插件可拦截核心接口:

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

官方文档列出的可拦截方法签名(写 @Signature 时 args 必须与之精确一致):

接口可拦截方法
Executorupdate(MappedStatement, Object)、query(MappedStatement, Object, RowBounds, ResultHandler)、flushStatements()、commit()、rollback()、getTransaction()、close()、isClosed()
ParameterHandlergetParameterObject()、setParameters(PreparedStatement)
ResultSetHandlerhandleResultSets(Statement)、handleOutputParameters(CallableStatement)
StatementHandlerprepare(Connection, Integer)、parameterize(Statement)、batch(Statement)、update(Statement)、query(Statement, ResultHandler)

插件的内部机制:责任链式 JDK 代理 ​

InterceptorChain.pluginAll() 在创建上述四大对象时逐层包装:每个插件生成一层 JDK 动态代理(Plugin 类),后注册的插件在外层。所以:

  • 多个分页/审计插件叠加时构成洋葱模型,执行顺序依赖注册次序,务必用集成测试锁定。
  • 官方文档警告:这些是底层类和方法,"如果你试图修改或覆盖某个方法的行为,你很可能会破坏 MyBatis 的核心"——只监控(不改返回值)相对安全,改 SQL/参数/结果就要全链路回归。

分页插件(PageHelper 一类)的实现原理与坑 ​

典型做法是拦截 Executor.query(...):先执行改写的 count 查询,再给原 SQL 拼 limit,并把分页结果放进线程上下文。两个注意事项:

  • 拦截 StatementHandler.prepare 还是 Executor.query,决定了它能否兼容嵌套结果映射与二级缓存,主流实现选后者。
  • ⚠️ PageHelper.startPage() 基于 ThreadLocal,只对紧接着的第一条查询生效;中间提前 return 或抛异常会让分页参数"泄漏"到当前线程的下一条查询——要么保证紧随查询,要么在 finally 中 clearPage()。

常见用途:

  • 分页插件。
  • SQL 审计。
  • 数据权限。
  • 多租户条件注入。
  • 慢 SQL 记录。
  • 字段加解密。

13.2 插件示例 ​

java
@Intercepts({
    @Signature(type = StatementHandler.class,
               method = "prepare",
               args = {Connection.class, Integer.class})
})
public class SqlLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
}

插件强大但危险。修改 SQL、参数或结果时,要考虑:

  • 是否影响所有 Mapper。
  • 是否破坏分页、缓存、批处理。
  • 是否兼容多数据源。
  • 是否导致 SQL 注入。
  • 是否方便测试和排查。

14. 执行器与批处理 ​

14.1 ExecutorType ​

类型特点使用场景
SIMPLE每次执行创建新 Statement默认,通用
REUSE复用 PreparedStatement同 SQL 重复执行
BATCH批量执行更新语句大量插入/更新

14.2 BATCH 使用 ​

原生 MyBatis:

java
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    for (User user : users) {
        mapper.insert(user);
    }
    session.flushStatements();
    session.commit();
}

Spring 中可配置专用 SqlSessionTemplate,但同一事务中不要混用不同 ExecutorType,否则可能产生事务和执行顺序问题。

14.3 批处理建议 ​

  • 每批控制在合理大小,如 500、1000、2000,需要压测决定。
  • 关注数据库连接超时、事务日志、锁等待。
  • 批量失败时要考虑如何定位失败数据。
  • 如果数据库支持多值插入,比较多值插入和 JDBC batch 的性能。

15. 分页 ​

15.1 物理分页 ​

推荐数据库层分页:

xml
select id, username, email
from users
where status = #{status}
order by id desc
limit #{limit} offset #{offset}

15.2 分页常见坑 ​

  • 深分页 offset 很大时性能差。
  • 复杂 join 加分页可能导致重复或遗漏。
  • 没有稳定排序会导致翻页结果漂移。
  • count 查询和列表查询条件不一致。
  • 动态排序字段使用 ${} 时必须白名单。

15.3 深分页优化 ​

sql
select id, username, email
from users
where id < #{lastSeenId}
order by id desc
limit #{pageSize}

这种基于游标或上一页最后一条记录的方式,适合按时间或自增 ID 递增浏览。

16. 复杂查询设计 ​

16.1 什么时候用 join ​

适合:

  • 列表页需要一次展示主表和少量关联字段。
  • 关联对象不需要完整生命周期。
  • 查询量大,需要减少网络往返。

不适合:

  • 一对多集合过大。
  • 需要对主表准确分页。
  • join 后字段过多导致传输膨胀。

16.2 什么时候拆查询 ​

适合:

  • 主表先分页,再按主键集合批量查子表。
  • 需要聚合多个来源的数据。
  • 子数据可缓存或可延迟加载。

示例流程:

  1. 查订单分页列表。
  2. 收集 user_id。
  3. 一次性 select * from users where id in (...)。
  4. 在 Java 中组装结果。

这种方式通常比 N+1 更稳定。

16.3 复杂 resultMap 的规范 ​

  • 主表列加别名,如 order_id、user_id,避免重名。
  • 每个嵌套对象配置 id。
  • 复杂映射写注释说明数据形态。
  • 不要把所有查询都塞进一个超级 resultMap。
  • 一个 XML 文件按聚合根或业务边界组织。

17. 日志、测试与调试 ​

17.1 打印 SQL ​

Spring Boot 配置:

yaml
logging:
  level:
    com.example.mapper: debug
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

注意:MyBatis 日志通常会分开打印 SQL 和参数,不一定直接输出完整可执行 SQL。

17.2 Mapper 测试 ​

建议用真实数据库或 Testcontainers 测试关键 SQL。

测试重点:

  • SQL 语法是否兼容目标数据库。
  • 动态条件组合是否正确。
  • 空参数是否符合预期。
  • resultMap 是否映射完整。
  • 事务回滚是否生效。
  • 批处理是否真的批量执行。

17.3 常见报错定位 ​

报错常见原因
Invalid bound statementnamespace、id、XML 路径、Mapper 扫描错误
Parameter not found多参数未使用 @Param 或名称写错
Result Maps collection already contains valueXML 重复加载或 id 重复
TooManyResultsExceptionselectOne 实际返回多行
SQLSyntaxErrorException动态 SQL 拼接后语法错误
BindingExceptionMapper 方法与语句绑定失败

18. 性能优化 ​

18.1 SQL 优先 ​

MyBatis 性能问题很多本质是 SQL 问题。

检查顺序:

  1. 是否命中索引。
  2. 是否返回过多列。
  3. 是否返回过多行。
  4. 是否存在 N+1。
  5. 是否使用了函数导致索引失效。
  6. 是否排序或分组成本过高。
  7. 是否有锁等待。

18.2 MyBatis 层优化 ​

  • 使用 resultMap 精确控制字段。
  • 大结果集使用 fetchSize、游标或流式处理。
  • 批量写入使用多值插入或 ExecutorType.BATCH。
  • 高频简单查询避免复杂嵌套映射。
  • 谨慎开启懒加载,配合 SQL 日志确认查询次数。
  • 只在明确读多写少场景使用二级缓存。

18.3 大结果集处理 ​

不要一次性把百万级数据加载成 List。可考虑:

  • 数据库分页循环处理。
  • MyBatis Cursor。
  • 服务端游标或流式 ResultSet。
  • 分批导出。

伪代码:

java
try (Cursor<User> cursor = userMapper.scanActiveUsers()) {
    for (User user : cursor) {
        process(user);
    }
}

大结果集要关注连接占用时间,避免长事务影响数据库。

19. 安全与规范 ​

19.1 SQL 注入防护 ​

安全规则:

  • 值参数一律使用 #{}。
  • 表名、列名、排序字段若必须动态,使用枚举白名单。
  • 不把前端传入字符串直接放入 ${}。
  • 模糊查询也使用参数绑定,不手动拼接 SQL 字符串。

19.2 XML 组织规范 ​

建议:

  • 一个 Mapper XML 对应一个 Mapper 接口。
  • namespace 使用接口全限定名。
  • SQL id 使用接口方法名。
  • resultMap 命名清晰,如 UserBaseMap、UserDetailMap。
  • 公共字段片段命名如 BaseColumns。
  • 复杂 SQL 前写简短注释。
  • 不在 XML 中写业务判断,只写查询条件判断。

19.3 Mapper 接口规范 ​

java
List<User> findByStatus(@Param("status") UserStatus status);
Optional<User> findOptionalById(@Param("id") Long id);
int updateEmail(@Param("id") Long id, @Param("email") String email);

建议:

  • 多参数使用 @Param。
  • 返回单条时明确是否允许为空。
  • 写操作返回影响行数。
  • 不把 Map<String,Object> 当万能参数长期使用。

20. 高级专题 ​

20.1 多数据源 ​

关键点:

  • 每个数据源通常对应独立 SqlSessionFactory。
  • Mapper 扫描包要按数据源隔离。
  • 事务管理器要绑定正确 DataSource。
  • 跨库事务需要额外方案,不能默认认为本地事务能覆盖多个库。

20.2 读写分离 ​

常见方式:

  • 数据源路由插件。
  • Spring AbstractRoutingDataSource。
  • 在 Service 层明确读库/写库。

注意:

  • 主从复制有延迟。
  • 写后立即读必须走主库或使用一致性策略。
  • 事务内通常应固定数据源。

20.3 多租户 ​

实现方式:

  • SQL 条件中显式添加 tenant_id。
  • 拦截器自动注入租户条件。
  • 物理分库分表。

高风险点:

  • 插件自动改 SQL 可能漏掉复杂子查询。
  • 管理后台、异步任务、数据修复脚本容易绕过租户条件。
  • 必须有测试覆盖越权访问。

20.4 分库分表 ​

MyBatis 本身不提供完整分库分表能力。通常结合:

  • ShardingSphere。
  • 中间件代理。
  • 自研路由层。

MyBatis 负责执行映射,路由和聚合由外部方案处理。

21. 常见坑清单 ​

  1. #{} 和 ${} 混淆,导致 SQL 注入。
  2. XML 没有被打包到 classpath。
  3. namespace 不等于接口全限定名。
  4. Mapper 方法名和 SQL id 不一致。
  5. 多参数不加 @Param。
  6. resultType 与 resultMap 同时使用。
  7. 一对多 join 后直接分页。
  8. 懒加载引发 N+1。
  9. 二级缓存用于高频更新表。
  10. Spring 管理下手动操作 SqlSession 事务。
  11. 批处理一次提交过大。
  12. 动态 SQL 生成空 where 或多余逗号。
  13. 没有稳定排序就做分页。
  14. Map 参数过度使用导致接口不可维护。
  15. 插件全局生效但缺少回归测试。

22. 面试与项目表达 ​

22.1 MyBatis 执行流程怎么说 ​

可以这样回答:

MyBatis 启动时读取全局配置和 Mapper XML,构建 SqlSessionFactory。运行时通过 SqlSession 获取 Mapper 代理,代理根据接口方法定位到 MappedStatement,解析动态 SQL,生成 BoundSql 和参数映射,然后用 JDBC 执行 SQL,最后通过 ResultMap 或 resultType 把结果集转换成 Java 对象。在 Spring 中,SqlSessionTemplate 会把会话绑定到当前事务并管理提交、回滚和关闭。

22.2 一级缓存和二级缓存区别 ​

一级缓存是 SqlSession 级别,默认启用,主要减少同一会话内重复查询。二级缓存是 Mapper namespace 级别,需要显式配置,能跨会话复用结果,但需要仔细处理更新失效和一致性。业务上读多写少、结果稳定才适合二级缓存。

22.3 如何避免 N+1 ​

  • 日志中观察 SQL 次数。
  • 列表页优先 join 或批量二次查询。
  • 一对多分页先查主表,再批量查子表。
  • 谨慎使用懒加载。
  • 对关联查询写专门 DTO,而不是复用复杂实体树。

22.4 MyBatis 插件原理 ​

MyBatis 插件基于拦截器机制,能拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 等核心对象的方法调用。分页、审计、数据权限等功能常基于插件实现。但插件是全局切面,必须控制作用范围并做好测试。

23. 实战练习 ​

23.1 基础练习 ​

  1. 创建 users 表,完成增删改查。
  2. 使用 @Param 实现多条件查询。
  3. 开启下划线转驼峰映射。
  4. 使用 useGeneratedKeys 回填主键。

23.2 进阶练习 ​

  1. 编写动态 SQL 搜索接口。
  2. 实现用户和订单的一对多映射。
  3. 对一对多列表做正确分页。
  4. 编写批量插入接口并压测批量大小。
  5. 使用 Testcontainers 测试 Mapper。

23.3 高级练习 ​

  1. 自定义枚举 TypeHandler。
  2. 写一个慢 SQL 拦截器。
  3. 对读多写少配置二级缓存并验证失效。
  4. 实现多数据源 Mapper 扫描隔离。
  5. 设计防 SQL 注入的动态排序白名单。

24. 推荐项目结构 ​

text
src/main/java/com/example
  domain/
    User.java
  mapper/
    UserMapper.java
  service/
    UserService.java
  config/
    MyBatisConfig.java
src/main/resources
  mapper/
    UserMapper.xml

大型项目可按业务模块拆分:

text
user/
  domain/
  mapper/
  service/
order/
  domain/
  mapper/
  service/

25. 官方资料 ​

基于 VitePress 构建的个人学习文档知识库