本文件覆盖 MyBatis 与 Spring / Spring Boot 的工程化实践:Starter 自动配置、Mapper 扫描、事务边界、SqlSession 生命周期、测试策略、代码生成、目录规范、多环境配置、主线项目基础落地。
官方参考:
- MyBatis Spring: https://mybatis.org/spring/
- MyBatis Spring Boot Starter: https://mybatis.org/spring-boot-starter/mybatis-spring-boot-starter/
1. 为什么需要 MyBatis-Spring
原生 MyBatis 需要手动创建 SqlSessionFactory 和 SqlSession。Spring 集成后:
- 由 Spring 管理
SqlSessionFactory。 - Mapper 接口成为 Spring Bean。
- 参与 Spring 事务。
- MyBatis 异常转换为 Spring
DataAccessException。 - 业务代码无需手动管理 SqlSession。
业务代码:
java
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public User getUser(Long id) {
return userMapper.selectById(id);
}
}
2. Spring Boot 最小 Demo
Maven:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
配置:
yaml
spring:
datasource:
url: jdbc:h2:mem:demo;MODE=MySQL;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.mybatis.domain
configuration:
map-underscore-to-camel-case: true
启动类:
java
@SpringBootApplication
@MapperScan("com.example.mybatis.mapper")
public class MyBatisApplication {
public static void main(String[] args) {
SpringApplication.run(MyBatisApplication.class, args);
}
}
3. 目录结构
text
src/main/java/com/example/mybatis/
├── MyBatisApplication.java
├── controller/
├── service/
├── domain/
├── dto/
├── mapper/
└── config/
src/main/resources/
├── mapper/
│ └── UserMapper.xml
├── schema.sql
└── application.yml
职责:
controller:HTTP 入参和响应。service:事务和业务流程。mapper:数据访问接口。domain:领域对象或数据库对象。dto:请求和响应对象。resources/mapper:SQL XML。
4. Mapper 扫描
方式一:启动类 @MapperScan:
java
@MapperScan("com.example.mybatis.mapper")
方式二:每个 Mapper 加 @Mapper:
java
@Mapper
public interface UserMapper {}
大型项目推荐 @MapperScan,减少重复注解。
5. Mapper XML 路径
yaml
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
常见错误:
- XML 没被打包到 classpath。
- namespace 与 Mapper 接口全限定名不一致。
- statement id 与方法名不一致。
- resultType 包名写错。
6. 事务边界
MyBatis-Spring 会让 MyBatis 参与 Spring 事务。
java
@Service
public class OrderService {
private final OrderMapper orderMapper;
private final StockMapper stockMapper;
@Transactional
public Long createOrder(CreateOrderCommand command) {
orderMapper.insert(command.toOrder());
stockMapper.decrease(command.productId(), command.quantity());
return command.orderId();
}
}
事务应放在 Service 层,而不是 Mapper 层。
7. 事务失效场景
常见失效:
- 同类内部方法自调用。
- 方法不是
public。 - 异常被捕获但未抛出。
- 抛出受检异常但未配置 rollback。
- 数据源不受 Spring 管理。
自调用反例:
java
public void outer() {
inner(); // 事务可能不生效
}
@Transactional
public void inner() {}
解决:
- 把事务方法放到另一个 Spring Bean。
- 通过代理调用。
- 使用编程式事务。
8. 只读事务
java
@Transactional(readOnly = true)
public UserDetail getUserDetail(Long id) {
return userMapper.selectDetail(id);
}
只读事务表达意图,并可让部分数据库或连接池做优化。但不要误以为它能绝对阻止写操作。
9. 批量操作
MyBatis 支持 ExecutorType.BATCH。
Spring 中可配置批量 SqlSessionTemplate,或在特定场景中使用批处理。
XML 批量插入:
xml
<insert id="batchInsert">
insert into users(username, email)
values
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>
专家提醒:
- 批量大小要控制。
- 大批量注意 SQL 长度限制。
- 批处理失败要处理部分成功。
- 大数据导入应考虑分批提交。
10. 分页
简单分页:
xml
<select id="selectPage" resultType="User">
select id, username, email
from users
order by id desc
limit #{limit} offset #{offset}
</select>
Mapper:
java
List<User> selectPage(@Param("limit") int limit, @Param("offset") int offset);
深分页问题:
sql
limit 20 offset 1000000
数据库需要跳过大量记录,性能差。
优化:
sql
where id < #{lastId}
order by id desc
limit #{limit}
称为游标分页或 seek pagination。
11. 测试策略
Mapper 测试:
java
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void selectById() {
User user = userMapper.selectById(1L);
assertThat(user.getUsername()).isEqualTo("ada");
}
}
Spring Boot 集成测试:
java
@SpringBootTest
@Transactional
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void createUser() {
Long id = userService.createUser(new CreateUserCommand("ada", "ada@example.com"));
assertThat(id).isNotNull();
}
}
12. Testcontainers
H2 与 MySQL/PostgreSQL 行为有差异。关键 SQL 建议用 Testcontainers 跑真实数据库。
java
@Testcontainers
@SpringBootTest
class UserMapperMysqlTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");
@DynamicPropertySource
static void datasource(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
}
13. SQL 初始化与迁移
简单 Demo 可用:
text
schema.sql
data.sql
生产项目建议使用:
- Flyway。
- Liquibase。
Flyway 示例:
text
src/main/resources/db/migration/V1__create_users.sql
src/main/resources/db/migration/V2__add_user_status.sql
数据库结构迁移必须版本化,不应手工改库。
14. 代码生成
MyBatis Generator 或 MyBatis Dynamic SQL 可生成基础 Mapper。
适合:
- 大量单表 CRUD。
- 数据库表多。
- 规范化基础代码。
风险:
- 生成代码被手工改坏。
- 复杂业务查询仍需手写。
- 生成模型与领域模型混淆。
专家实践:生成代码放基础访问层,业务查询单独写,领域模型不要完全等同数据库表。
15. DTO、DO、Domain 分层
常见对象:
- Request DTO:HTTP 入参。
- Response DTO:HTTP 响应。
- DO / PO:数据库表映射对象。
- Domain:业务领域对象。
- Command / Query:应用服务入参。
简单项目可以合并,但复杂项目应避免 Controller 直接暴露数据库对象。
16. 异常处理
MyBatis-Spring 会将异常转换为 Spring 的 DataAccessException 层级。
业务层不要把所有异常吞掉:
java
try {
userMapper.insert(user);
} catch (DuplicateKeyException e) {
throw new BusinessException("用户名已存在");
}
17. 日志配置
开发环境打印 SQL:
yaml
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
生产环境建议走 SLF4J,并注意不要打印敏感参数。
yaml
logging:
level:
com.example.mybatis.mapper: debug
18. 主线项目 Stage 1:用户模块
目标:构建 KnowledgeHub 的用户模块。
表:
sql
create table users (
id bigint generated by default as identity primary key,
username varchar(64) not null unique,
email varchar(128) not null,
status varchar(32) not null,
created_at timestamp not null
);
Mapper:
java
public interface UserMapper {
UserDO selectById(Long id);
int insert(UserDO user);
List<UserDO> search(UserSearchQuery query);
}
Service:
java
@Service
public class UserService {
private final UserMapper userMapper;
@Transactional
public Long createUser(CreateUserCommand command) {
UserDO user = UserDO.create(command.username(), command.email());
userMapper.insert(user);
return user.getId();
}
}
19. 工程化知识清单
入门:
- Starter 依赖。
- application.yml。
- MapperScan。
- XML 路径。
- Service 调 Mapper。
进阶:
- Spring 事务。
- Mapper 测试。
- 分页。
- 批量插入。
- SQL 初始化。
高级:
- Testcontainers。
- Flyway/Liquibase。
- 异常转换。
- DTO/DO 分层。
- 代码生成治理。
精通:
- 多环境配置。
- 连接池。
- 批处理策略。
- 深分页优化。
- 日志脱敏。
专家:
- 数据访问层架构。
- 事务边界治理。
- 多数据源。
- 数据迁移规范。
- SQL 审查流程。
20. 面试题与完整答案
20.1 MyBatis-Spring 的作用是什么?
它把 MyBatis 集成到 Spring 容器中,让 Mapper 成为 Spring Bean,让 SqlSession 参与 Spring 事务,并把 MyBatis 异常转换为 Spring DataAccessException。业务代码不需要手动管理 SqlSession。
20.2 @MapperScan 和 @Mapper 如何选择?
少量 Mapper 可用 @Mapper。大型项目推荐 @MapperScan 扫描包,减少重复注解,统一管理 Mapper 注册路径。
20.3 事务应该放在哪一层?
事务应放在 Service 或应用服务层,因为事务代表业务操作边界。Mapper 只负责单条或少量 SQL,不应决定业务事务范围。
20.4 Spring 事务为什么会失效?
常见原因包括同类内部自调用、方法非 public、异常被捕获、受检异常未配置 rollback、对象不是 Spring Bean、数据源不受 Spring 管理。核心原因是没有通过 Spring 事务代理执行。
20.5 为什么关键 SQL 测试不建议只用 H2?
H2 与 MySQL/PostgreSQL 在 SQL 方言、索引、锁、时间类型、分页、JSON、关键字等方面可能不同。关键 SQL 应使用 Testcontainers 跑真实数据库,降低上线风险。
20.6 批量插入有哪些风险?
SQL 过长、参数过多、锁时间长、失败后部分成功、内存占用高。应控制批量大小,分批提交,并设计失败处理策略。
20.7 深分页为什么慢?
limit offset 的 offset 很大时,数据库需要扫描并跳过大量记录。优化方式是基于索引字段做游标分页,如 where id < lastId order by id desc limit size。
20.8 为什么要用 Flyway 或 Liquibase?
生产数据库结构需要版本化、可审计、可回滚和可重复部署。手工改库不可追踪,容易导致环境不一致。Flyway/Liquibase 能把数据库变更纳入工程流程。
21. Spring Boot 自动配置机制
MyBatis Spring Boot Starter 通常会自动配置:
SqlSessionFactorySqlSessionTemplate- Mapper 扫描支持
- MyBatis 配置属性绑定
常用属性:
yaml
mybatis:
config-location: classpath:mybatis-config.xml
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.domain
type-handlers-package: com.example.mybatis.typehandler
configuration:
map-underscore-to-camel-case: true
如果同时配置 config-location 和 configuration,要注意配置来源冲突。团队应统一一种配置风格。
22. SqlSessionTemplate
Spring 中不直接使用原生 SqlSession,而是使用线程安全的 SqlSessionTemplate。
它负责:
- 获取当前事务绑定的 SqlSession。
- 提交或回滚交给 Spring 事务。
- 关闭 SqlSession。
- 异常转换。
业务代码通常不直接注入 SqlSessionTemplate,而是注入 Mapper。
23. 事务传播行为
常见传播:
REQUIRED:默认,加入当前事务,没有则新建。REQUIRES_NEW:挂起当前事务,新建事务。NESTED:嵌套事务,依赖 savepoint。SUPPORTS:有事务就加入,没有也可执行。
示例:
java
@Transactional
public void createOrder() {
orderMapper.insert(order);
auditService.writeAuditLog(); // 如果内部 REQUIRES_NEW,日志独立提交
}
专家提醒:REQUIRES_NEW 会额外占用连接,滥用可能导致连接池耗尽。
24. 回滚规则
Spring 默认对 RuntimeException 和 Error 回滚,对 checked exception 不回滚。
java
@Transactional(rollbackFor = Exception.class)
public void importUsers(File file) throws IOException {
// ...
}
不要吞异常:
java
try {
mapper.insert(row);
} catch (Exception e) {
log.error("failed", e);
}
吞掉异常会让事务认为执行成功。
25. Service 方法设计
好的 Service 方法:
- 表达业务用例。
- 控制事务。
- 调用一个或多个 Mapper/Repository。
- 做权限和业务校验。
- 不拼 SQL。
java
@Transactional
public Long enrollCourse(Long userId, Long courseId) {
CourseDO course = courseMapper.selectByIdForUpdate(courseId);
if (!course.canEnroll()) {
throw new BusinessException("课程不可报名");
}
enrollmentMapper.insert(userId, courseId);
courseMapper.increaseLearnerCount(courseId);
return courseId;
}
26. Mapper 测试数据准备
推荐每个测试显式准备数据:
java
@Sql("/sql/user-fixture.sql")
@MybatisTest
class UserMapperTest {}
或使用 Testcontainers + Flyway 初始化。
测试应覆盖:
- 正常查询。
- 空结果。
- 动态条件组合。
- 插入主键回填。
- 唯一约束冲突。
- 复杂 ResultMap。
27. 工程规范补充
Mapper 方法命名:
selectByIdselectPagesearchByConditioninsertupdateSelectivedeleteById
业务语义查询:
selectPublishedCoursesselectUserLearningProgressselectOrderSummary
不要:
query1listgetDataselectMap
28. 多模块项目组织
text
knowledge-api
knowledge-application
knowledge-domain
knowledge-infrastructure
MyBatis Mapper 可放 infrastructure:
text
knowledge-infrastructure/
├── mapper/
├── repository/
└── persistence-object/
领域层不依赖 MyBatis。
29. 工程化专家题补充
29.1 为什么事务方法不应包含远程调用?
远程调用耗时不可控,会延长数据库连接和锁持有时间,增加死锁和连接池耗尽风险。应尽量在事务外完成远程调用,或使用本地消息表、事件、最终一致性方案。
29.2 Mapper 测试为什么要覆盖动态 SQL 分支?
动态 SQL 的错误通常只在特定条件组合下出现,如多余 and、空集合、缺少 where、参数名错误。覆盖不同分支可以提前发现生产 SQL 错误。
29.3 代码生成如何治理?
生成代码应可重复生成,避免手工修改生成文件。业务扩展写在独立 Mapper 或扩展文件中。生成器配置纳入版本管理,生成代码和手写代码边界清晰。