03 MyBatis Spring Boot 集成、事务、测试与工程化体系

本文件覆盖 MyBatis 与 Spring / Spring Boot 的工程化实践:Starter 自动配置、Mapper 扫描、事务边界、SqlSession 生命周期、测试策略、代码生成、目录规范、多环境配置、主线项目基础落地。

官方参考:

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. 事务失效场景

常见失效:

  1. 同类内部方法自调用。
  2. 方法不是 public。
  3. 异常被捕获但未抛出。
  4. 抛出受检异常但未配置 rollback。
  5. 数据源不受 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 通常会自动配置:

  • SqlSessionFactory
  • SqlSessionTemplate
  • 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 方法命名:

  • selectById
  • selectPage
  • searchByCondition
  • insert
  • updateSelective
  • deleteById

业务语义查询:

  • selectPublishedCourses
  • selectUserLearningProgress
  • selectOrderSummary

不要:

  • query1
  • list
  • getData
  • selectMap

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 或扩展文件中。生成器配置纳入版本管理,生成代码和手写代码边界清晰。

相关推荐
imDwAaY2 小时前
微信运动每天晚上 10 点推送排行榜,技术上到底有多难?
后端
ServBay2 小时前
PHP 突爆 9.2 分高危漏洞,你升级了吗?
后端·php
武子康2 小时前
撤回发生在重建途中,旧索引还能切回来吗
人工智能·后端·agent
Thomas.Sir2 小时前
第2课:SpringBoot4.x 项目构建:Maven/Gradle配置详解,父工程与依赖管理
java·spring boot
码事漫谈3 小时前
双十一,我为什么想买一台编码显示器
后端
喵个咪3 小时前
GoWind Kanban 拆解:看板拖拽背后——LexoRank、乐观锁与尽力而为广播
后端·架构·go
喵个咪3 小时前
GoWind OA 拆解:用 3000 行 Go 写一个够用的审批工作流引擎
后端·架构·go
喵个咪3 小时前
GoWind Ledger 拆解:单式记账的余额一致性——confirm、补偿,以及没进事务的那些缝隙
后端·架构·go
喵个咪3 小时前
GoWind Shop 拆解:订单超时关单——一个事务里的四联动
后端·架构·go
我命由我123453 小时前
Python 开发 - Python 虚拟环境
运维·开发语言·后端·python·http·https·java-ee