外观
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、插件扩展等。
学习顺序建议:
- 先理解 JDBC、
PreparedStatement、事务、连接池、SQL 基础。 - 再掌握 MyBatis 的四个核心对象:
SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Mapper。 - 继续学习 XML Mapper、参数绑定、结果映射、动态 SQL。
- 进入缓存、批处理、分页、插件、类型处理器、Spring 集成。
- 最后学习性能分析、复杂映射、读写一致性、项目规范与常见坑。
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 一次查询的典型流程
- 应用启动时读取
mybatis-config.xml。 - 解析全局配置、环境、Mapper XML、Mapper 接口。
- 创建
SqlSessionFactory。 - 调用时打开
SqlSession。 - 通过 Mapper 代理调用接口方法。
- MyBatis 根据
namespace + id找到 mapped statement。 - 解析动态 SQL,生成最终 SQL 与参数映射。
- 使用 JDBC
PreparedStatement执行。 - 将
ResultSet映射为 Java 对象。 - 按事务策略提交、回滚、关闭会话。
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 还有几个不常用但很重要的属性(官方文档全量):
| 属性 | 默认值 | 说明 |
|---|---|---|
statementType | PREPARED | 可选 STATEMENT(不预编译)、PREPARED、CALLABLE(存储过程) |
resultSetType | 驱动决定 | FORWARD_ONLY / SCROLL_SENSITIVE / SCROLL_INSENSITIVE;流式读取时配合 fetchSize 使用 |
resultOrdered | false | 仅对嵌套 select 有意义:声明结果已按主行分组排列,让 MyBatis 提前释放主行引用,大幅降低大结果集嵌套映射的内存占用 |
databaseId | 无 | 配合 databaseIdProvider,同名语句可按数据库厂商提供不同 SQL |
affectData | false | 3.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 更清晰 |
| 复杂 resultMap | XML 更适合 |
| 团队希望 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/> 一行配置背后是官方文档定义的默认行为,可调属性:
| 属性 | 默认值 | 说明 |
|---|---|---|
eviction | LRU | 可选 LRU / FIFO / SOFT / WEAK |
flushInterval | 未设置 | 单位毫秒;不设置则只被语句调用刷新 |
size | 1024 | 缓存的对象引用/列表条数,注意对象大小与环境内存 |
readOnly | false | true 返回同一实例(快但调用方不能改);false 返回序列化副本(安全但慢) |
内部机制与陷阱:
- 缓存与 namespace 绑定:同 namespace 的所有语句共享一份缓存;
select默认useCache="true"、flushCache="false",insert/update/delete默认flushCache="true"(执行即清空本 namespace 缓存——粒度是命名空间级,不是行级/键级,改一行也会让该 namespace 全部查询缓存作废)。 - 二级缓存是事务性的:结果只在
SqlSessioncommit 后才进入二级缓存;未提交会话读不到别人还没提交的数据。 - ⚠️ 只缓存本 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 插件可拦截核心接口:
ExecutorStatementHandlerParameterHandlerResultSetHandler
官方文档列出的可拦截方法签名(写 @Signature 时 args 必须与之精确一致):
| 接口 | 可拦截方法 |
|---|---|
Executor | update(MappedStatement, Object)、query(MappedStatement, Object, RowBounds, ResultHandler)、flushStatements()、commit()、rollback()、getTransaction()、close()、isClosed() |
ParameterHandler | getParameterObject()、setParameters(PreparedStatement) |
ResultSetHandler | handleResultSets(Statement)、handleOutputParameters(CallableStatement) |
StatementHandler | prepare(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 什么时候拆查询
适合:
- 主表先分页,再按主键集合批量查子表。
- 需要聚合多个来源的数据。
- 子数据可缓存或可延迟加载。
示例流程:
- 查订单分页列表。
- 收集
user_id。 - 一次性
select * from users where id in (...)。 - 在 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 statement | namespace、id、XML 路径、Mapper 扫描错误 |
| Parameter not found | 多参数未使用 @Param 或名称写错 |
| Result Maps collection already contains value | XML 重复加载或 id 重复 |
| TooManyResultsException | selectOne 实际返回多行 |
| SQLSyntaxErrorException | 动态 SQL 拼接后语法错误 |
| BindingException | Mapper 方法与语句绑定失败 |
18. 性能优化
18.1 SQL 优先
MyBatis 性能问题很多本质是 SQL 问题。
检查顺序:
- 是否命中索引。
- 是否返回过多列。
- 是否返回过多行。
- 是否存在 N+1。
- 是否使用了函数导致索引失效。
- 是否排序或分组成本过高。
- 是否有锁等待。
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. 常见坑清单
#{}和${}混淆,导致 SQL 注入。- XML 没有被打包到 classpath。
namespace不等于接口全限定名。- Mapper 方法名和 SQL
id不一致。 - 多参数不加
@Param。 resultType与resultMap同时使用。- 一对多 join 后直接分页。
- 懒加载引发 N+1。
- 二级缓存用于高频更新表。
- Spring 管理下手动操作
SqlSession事务。 - 批处理一次提交过大。
- 动态 SQL 生成空
where或多余逗号。 - 没有稳定排序就做分页。
Map参数过度使用导致接口不可维护。- 插件全局生效但缺少回归测试。
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 基础练习
- 创建
users表,完成增删改查。 - 使用
@Param实现多条件查询。 - 开启下划线转驼峰映射。
- 使用
useGeneratedKeys回填主键。
23.2 进阶练习
- 编写动态 SQL 搜索接口。
- 实现用户和订单的一对多映射。
- 对一对多列表做正确分页。
- 编写批量插入接口并压测批量大小。
- 使用 Testcontainers 测试 Mapper。
23.3 高级练习
- 自定义枚举 TypeHandler。
- 写一个慢 SQL 拦截器。
- 对读多写少配置二级缓存并验证失效。
- 实现多数据源 Mapper 扫描隔离。
- 设计防 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. 官方资料
- MyBatis Getting Started:https://mybatis.org/mybatis-3/getting-started.html
- MyBatis Configuration:https://mybatis.org/mybatis-3/configuration.html
- MyBatis Mapper XML:https://mybatis.org/mybatis-3/sqlmap-xml.html
- MyBatis Dynamic SQL:https://mybatis.org/mybatis-3/dynamic-sql.html
- MyBatis Java API:https://mybatis.org/mybatis-3/java-api.html
- MyBatis-Spring SqlSession:https://mybatis.org/spring/sqlsession.html
- MyBatis-Spring Transactions:https://mybatis.org/spring/transactions.html
- MyBatis-Spring Mappers:https://mybatis.org/spring/mappers.html
- MyBatis Spring Boot Starter:https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/