适用读者:能跑通 MyBatis,但说不清"SqlSession 谁在管""注解和 XML 该怎么分工"的人。
前置:S08-B01(Mapper 为什么不用写实现类)、B02(参数与结果映射)、B03(动态 SQL)、S07(Spring IoC 与代理)。
覆盖旧文(章号版):3054(第1章 框架介绍)、3055(第2章 环境搭建)、3056(第3章 第一个程序)、3057(第4章 核心配置文件)、3058(第5章 SQL 映射)、3065(第12章 注解开发)、3066(第13章 Spring 集成)、3067(第14章 Spring Boot 集成)、3074(第21章 SQL 构建器)、3075(第22章 Boot 集成进阶)、3079(第26章 测试与调试)→ 11 章 / 约 7.4 万字符。
扩展点:E-S08-021(三种集成形态与 SqlSession 的生命周期归属)、E-S08-022(注解 vs XML vs SQL 构建器的分工判据)、E-S08-023(可测性:@MybatisTest与 H2 的边界)。
下一篇:S08-J06 · MyBatis 进阶机制。
1. 一句话结论
MyBatis 与 Spring 集成的全部分歧,只有一个问题:SqlSession 由谁创建、由谁关闭、什么时候提交。答案确定后,剩下的(配置怎么放、注解还是 XML、事务怎么生效)都是推论。
出处:
A-3066(Spring 集成)、A-3067(Boot 集成)、A-3075(Boot 进阶)。
2. 三种集成形态与生命周期归属(E-S08-021)
| 形态 | 谁管 SqlSession | 事务 | 适用 |
|---|---|---|---|
原生(SqlSessionFactory + 手动 openSession) |
你自己(try-with-resources) |
session.commit() 手动 |
学习、批处理脚本、无 Spring |
Spring 集成(SqlSessionTemplate / MapperFactoryBean) |
Spring(线程绑定,随事务提交) | @Transactional |
传统 Spring 项目 |
Spring Boot 自动配置(mybatis-spring-boot-starter) |
Spring(同上)+ 自动扫描 @Mapper |
同上 | 现代项目默认 |
// 原生:必须自己关心关闭与提交
try (SqlSession s = factory.openSession()) {
UserMapper m = s.getMapper(UserMapper.class);
m.insert(u);
s.commit(); // 忘了这行 → 数据不见
}
// Spring:交给容器,事务由 @Transactional 决定
@Service
public class UserService {
private final UserMapper mapper; // 构造注入(Spring 4.3+ 单构造免注解)
@Transactional
public void create(User u) { mapper.insert(u); }
}
三条硬结论:
① SqlSessionTemplate 是线程安全的;原生 SqlSession 不是 → 不要把它当字段存
② 不开事务时,每次 Mapper 调用自动提交(Spring 环境下)
③ 混用「原生 openSession」与「Spring 事务」→ 两个连接、两套事务,回滚各管各的
判据:"为什么我的 insert 没生效"九成是漏了 commit() 或事务边界写在了内层私有方法上(同类自调用不走代理,见 S07-J01)。
3. 核心配置文件与 Boot 配置的对应
原生 mybatis-config.xml |
Boot application.yml |
|---|---|
<environments> 数据源 |
spring.datasource.* |
<mappers><mapper resource=…> |
mybatis.mapper-locations=classpath:mapper/*.xml |
<typeAliases> |
mybatis.type-aliases-package |
<settings><setting name="mapUnderscoreToCamelCase"> |
mybatis.configuration.map-underscore-to-camel-case=true |
<plugins> |
@Bean Interceptor 或 mybatis.configuration + 注册 |
坑 1:mybatis.configuration.* 与 mybatis-config.xml 同时存在时以 yml 为准(Boot 的自动配置会覆盖),排查"下划线转驼峰不生效"先看这里。
4. 注解 vs XML vs SQL 构建器(E-S08-022)
| 方式 | 优点 | 缺点 | 判据 |
|---|---|---|---|
注解(@Select/@Insert/@Results) |
就近、无需 XML、类型安全 | 动态 SQL 难写(要嵌 <script>)、复杂结果映射拥挤 |
简单 CRUD、单表 |
| XML 映射 | 动态 SQL 表达力最强、SQL 与代码分离、易 review | 拼写错误运行时才暴露、两处维护 | 复杂查询、多表、动态条件 |
SQL 构建器(new SQL(){{…}} 或 @SelectProvider) |
类型安全、可复用片段 | 可读性差(匿名子类初始化块)、复杂 SQL 反而更难读 | 条件组合多但结构固定 |
// 注解 + 动态 SQL 的折中:用 <script>
@Select("<script>SELECT * FROM user <where>" +
"<if test='name != null'>AND name = #{name}</if></where></script>")
List<User> find(@Param("name") String name);
// SQL 构建器:适合"条件可选"的查询
String sql = new SQL() {{
SELECT("*"); FROM("user");
if (name != null) WHERE("name = #{name}");
}}.toString();
判据:一张表超过 10 个字段或查询有 3 个以上可选条件,就上 XML——注解里的字符串拼接是维护灾难。
5. 结果映射的三层(3057/3058 的压缩)
① 自动映射:列名 == 属性名(或开启 mapUnderscoreToCamelCase)
② 别名映射:SELECT user_name AS userName
③ 显式 resultMap:类型转换、嵌套映射、构造器注入
<resultMap id="userWithOrders" type="User">
<id property="id" column="id"/>
<result property="createdAt" column="created_at"
javaType="java.time.LocalDateTime"/>
<collection property="orders" ofType="Order"
select="selectOrdersByUserId" column="id"/> <!-- 嵌套查询 → N+1 -->
</resultMap>
判据:<collection select=…> 就是 N+1 的源头(见 J06 §3 的 join 替代方案);能用 <collection resultMap=…> 的 join 映射就别用嵌套查询。
6. 可测性:@MybatisTest 与 H2 的边界(E-S08-023)
@MybatisTest // 只加载 MyBatis 相关 Bean,不起整个应用
@AutoConfigureTestDatabase(replace = NONE) // 指向真实测试库
class UserMapperTest {
@Autowired UserMapper mapper;
@Test void findsByCondition() { … }
}
| 方案 | 优点 | 边界 |
|---|---|---|
| H2/内存库 | 快、无外部依赖 | 方言差异:ON DUPLICATE KEY、窗口函数、JSON 函数、EXPLAIN 行为不同 → 测不出真实 SQL 问题 |
| Testcontainers MySQL | 方言一致、可跑真实 DDL | 慢(数秒启动)、CI 需 Docker |
| 真实测试库 | 最接近生产 | 隔离与清理成本高 |
判据:凡是用了 MySQL 特有语法(ON DUPLICATE KEY UPDATE、INSERT IGNORE、JSON 列、全文索引)的 Mapper,必须用 Testcontainers 或真实库测;H2 只适合纯标准 SQL 的冒烟。
调试三板:
① 日志:logging.level.<mapper 包>=DEBUG(打印 SQL 与参数)
② p6spy / datasource-proxy:打印耗时与真实参数(看清 ? 绑定了什么)
③ MyBatis 内置:configuration.setLogImpl(StdOutImpl) 在无日志框架时兜底
7. 反例:六种"集成后更难查"
| 现象 | 根因 |
|---|---|
| 事务回滚了但数据还在 | 混用原生 SqlSession(各自连接、各自提交) |
@Transactional 不生效 |
同类自调用 / 方法非 public / 异常被吞 |
| 下划线转驼峰失效 | yml 与 XML 同时配置,被覆盖 |
| Mapper 注入失败 | 未加 @Mapper 且未配 @MapperScan |
| SQL 在 XML 改了不生效 | 打包时 XML 未进 target/classes(resources 配置) |
| 批量插入慢 | 循环调用单条 insert(应 ExecutorType.BATCH 或 <foreach>) |
8. 自测
- 三种集成形态下 SqlSession 谁管?为什么不要把它当字段?
- 注解/XML/构建器的分工判据?
mybatis.configuration与mybatis-config.xml冲突时谁生效?- H2 测不出的问题有哪些?
@Transactional不生效的三个常见原因?
答案
- 自己/Spring/Spring 自动配置;原生 SqlSession 非线程安全且会话状态会被并发污染。
- 简单单表用注解,复杂动态查询用 XML,条件组合多结构固定用构建器。
- yml(Boot 自动配置覆盖)。
- 方言特有语法与执行计划行为。
- 同类自调用不走代理、非 public、异常被 catch 未抛出。
9. 下一篇
S08-J06 · MyBatis 进阶机制 —— 关联映射与 N+1、一级/二级缓存的失效条件与脏读风险、插件四大接口与责任链顺序、批量的三种写法与性能实测口径。
本篇属于系列《S08 · MyBatis 持久层》。旧文来源:A-3054–3058、A-3065–3067、A-3074/3075/3079(章号版 11 章);扩展点 E-S08-021/022/023。版本口径 MyBatis 3.5.x / mybatis-spring-boot-starter 3.x / MySQL 8.0。