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

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

官方参考:

1. 为什么需要 MyBatis-Spring

原生 MyBatis 需要手动创建 SqlSessionFactorySqlSession。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-locationconfiguration,要注意配置来源冲突。团队应统一一种配置风格。

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

相关推荐
ElonMuscle1 小时前
GO环境速建笔记
后端
用户298698530141 小时前
Java 从零生成 Word 文档:段落、图片与表格操作
java·后端
SimonKing2 小时前
OpenCode 在 IDEA 中使用 ACP 协议 VS 直接使用 TUI,哪个编程方式更是你的菜?
java·后端·程序员
Gopher_HBo2 小时前
Disruptor多生产者多消费者分析
后端
杨运交2 小时前
[013][缓存模块]基于Redis的计数器缓存模板设计——AbstractCounterCacheTemplate 技术解析
spring boot·后端
IVEN_2 小时前
Gradle 依赖下载 403 Forbidden 修复:全局镜像配置实战
android·后端
用户762352425912 小时前
Innodb底层原理与Mysql日志机制深入剖析
后端