本文件覆盖 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 异常转换为 SpringDataAccessException。业务代码无需手动管理 SqlSession。业务代码ServicepublicclassUserService{privatefinalUserMapperuserMapper;publicUserService(UserMapperuserMapper){this.userMapperuserMapper;}publicUsergetUser(Longid){returnuserMapper.selectById(id);}}2. Spring Boot 最小 DemoMavendependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion3.0.3/version/dependencydependencygroupIdcom.h2database/groupIdartifactIdh2/artifactIdscoperuntime/scope/dependency/dependencies配置spring:datasource:url:jdbc:h2:mem:demo;MODEMySQL;DB_CLOSE_DELAY-1driver-class-name:org.h2.Driverusername:sapassword:mybatis:mapper-locations:classpath*:mapper/**/*.xmltype-aliases-package:com.example.mybatis.domainconfiguration:map-underscore-to-camel-case:true启动类SpringBootApplicationMapperScan(com.example.mybatis.mapper)publicclassMyBatisApplication{publicstaticvoidmain(String[]args){SpringApplication.run(MyBatisApplication.class,args);}}3. 目录结构src/main/java/com/example/mybatis/ ├── MyBatisApplication.java ├── controller/ ├── service/ ├── domain/ ├── dto/ ├── mapper/ └── config/ src/main/resources/ ├── mapper/ │ └── UserMapper.xml ├── schema.sql └── application.yml职责controllerHTTP 入参和响应。service事务和业务流程。mapper数据访问接口。domain领域对象或数据库对象。dto请求和响应对象。resources/mapperSQL XML。4. Mapper 扫描方式一启动类MapperScanMapperScan(com.example.mybatis.mapper)方式二每个 Mapper 加MapperMapperpublicinterfaceUserMapper{}大型项目推荐MapperScan减少重复注解。5. Mapper XML 路径mybatis:mapper-locations:classpath*:mapper/**/*.xml常见错误XML 没被打包到 classpath。namespace 与 Mapper 接口全限定名不一致。statement id 与方法名不一致。resultType 包名写错。6. 事务边界MyBatis-Spring 会让 MyBatis 参与 Spring 事务。ServicepublicclassOrderService{privatefinalOrderMapperorderMapper;privatefinalStockMapperstockMapper;TransactionalpublicLongcreateOrder(CreateOrderCommandcommand){orderMapper.insert(command.toOrder());stockMapper.decrease(command.productId(),command.quantity());returncommand.orderId();}}事务应放在 Service 层而不是 Mapper 层。7. 事务失效场景常见失效同类内部方法自调用。方法不是public。异常被捕获但未抛出。抛出受检异常但未配置 rollback。数据源不受 Spring 管理。自调用反例publicvoidouter(){inner();// 事务可能不生效}Transactionalpublicvoidinner(){}解决把事务方法放到另一个 Spring Bean。通过代理调用。使用编程式事务。8. 只读事务Transactional(readOnlytrue)publicUserDetailgetUserDetail(Longid){returnuserMapper.selectDetail(id);}只读事务表达意图并可让部分数据库或连接池做优化。但不要误以为它能绝对阻止写操作。9. 批量操作MyBatis 支持ExecutorType.BATCH。Spring 中可配置批量 SqlSessionTemplate或在特定场景中使用批处理。XML 批量插入insertidbatchInsertinsert into users(username, email) valuesforeachcollectionusersitemuserseparator,(#{user.username}, #{user.email})/foreach/insert专家提醒批量大小要控制。大批量注意 SQL 长度限制。批处理失败要处理部分成功。大数据导入应考虑分批提交。10. 分页简单分页selectidselectPageresultTypeUserselect id, username, email from users order by id desc limit #{limit} offset #{offset}/selectMapperListUserselectPage(Param(limit)intlimit,Param(offset)intoffset);深分页问题limit20offset1000000数据库需要跳过大量记录性能差。优化whereid#{lastId}orderbyiddesclimit#{limit}称为游标分页或 seek pagination。11. 测试策略Mapper 测试MybatisTestAutoConfigureTestDatabase(replaceAutoConfigureTestDatabase.Replace.NONE)classUserMapperTest{AutowiredprivateUserMapperuserMapper;TestvoidselectById(){UseruseruserMapper.selectById(1L);assertThat(user.getUsername()).isEqualTo(ada);}}Spring Boot 集成测试SpringBootTestTransactionalclassUserServiceTest{AutowiredprivateUserServiceuserService;TestvoidcreateUser(){LongiduserService.createUser(newCreateUserCommand(ada,adaexample.com));assertThat(id).isNotNull();}}12. TestcontainersH2 与 MySQL/PostgreSQL 行为有差异。关键 SQL 建议用 Testcontainers 跑真实数据库。TestcontainersSpringBootTestclassUserMapperMysqlTest{ContainerstaticMySQLContainer?mysqlnewMySQLContainer(mysql:8.4);DynamicPropertySourcestaticvoiddatasource(DynamicPropertyRegistryregistry){registry.add(spring.datasource.url,mysql::getJdbcUrl);registry.add(spring.datasource.username,mysql::getUsername);registry.add(spring.datasource.password,mysql::getPassword);}}13. SQL 初始化与迁移简单 Demo 可用schema.sql data.sql生产项目建议使用Flyway。Liquibase。Flyway 示例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 DTOHTTP 入参。Response DTOHTTP 响应。DO / PO数据库表映射对象。Domain业务领域对象。Command / Query应用服务入参。简单项目可以合并但复杂项目应避免 Controller 直接暴露数据库对象。16. 异常处理MyBatis-Spring 会将异常转换为 Spring 的DataAccessException层级。业务层不要把所有异常吞掉try{userMapper.insert(user);}catch(DuplicateKeyExceptione){thrownewBusinessException(用户名已存在);}17. 日志配置开发环境打印 SQLmybatis:configuration:log-impl:org.apache.ibatis.logging.stdout.StdOutImpl生产环境建议走 SLF4J并注意不要打印敏感参数。logging:level:com.example.mybatis.mapper:debug18. 主线项目 Stage 1用户模块目标构建KnowledgeHub的用户模块。表createtableusers(idbigintgeneratedbydefaultasidentityprimarykey,usernamevarchar(64)notnullunique,emailvarchar(128)notnull,statusvarchar(32)notnull,created_attimestampnotnull);MapperpublicinterfaceUserMapper{UserDOselectById(Longid);intinsert(UserDOuser);ListUserDOsearch(UserSearchQueryquery);}ServiceServicepublicclassUserService{privatefinalUserMapperuserMapper;TransactionalpublicLongcreateUser(CreateUserCommandcommand){UserDOuserUserDO.create(command.username(),command.email());userMapper.insert(user);returnuser.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.2MapperScan和Mapper如何选择少量 Mapper 可用Mapper。大型项目推荐MapperScan扫描包减少重复注解统一管理 Mapper 注册路径。20.3 事务应该放在哪一层事务应放在 Service 或应用服务层因为事务代表业务操作边界。Mapper 只负责单条或少量 SQL不应决定业务事务范围。20.4 Spring 事务为什么会失效常见原因包括同类内部自调用、方法非 public、异常被捕获、受检异常未配置 rollback、对象不是 Spring Bean、数据源不受 Spring 管理。核心原因是没有通过 Spring 事务代理执行。20.5 为什么关键 SQL 测试不建议只用 H2H2 与 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 通常会自动配置SqlSessionFactorySqlSessionTemplateMapper 扫描支持MyBatis 配置属性绑定常用属性mybatis:config-location:classpath:mybatis-config.xmlmapper-locations:classpath*:mapper/**/*.xmltype-aliases-package:com.example.domaintype-handlers-package:com.example.mybatis.typehandlerconfiguration:map-underscore-to-camel-case:true如果同时配置config-location和configuration要注意配置来源冲突。团队应统一一种配置风格。22. SqlSessionTemplateSpring 中不直接使用原生SqlSession而是使用线程安全的SqlSessionTemplate。它负责获取当前事务绑定的 SqlSession。提交或回滚交给 Spring 事务。关闭 SqlSession。异常转换。业务代码通常不直接注入SqlSessionTemplate而是注入 Mapper。23. 事务传播行为常见传播REQUIRED默认加入当前事务没有则新建。REQUIRES_NEW挂起当前事务新建事务。NESTED嵌套事务依赖 savepoint。SUPPORTS有事务就加入没有也可执行。示例TransactionalpublicvoidcreateOrder(){orderMapper.insert(order);auditService.writeAuditLog();// 如果内部 REQUIRES_NEW日志独立提交}专家提醒REQUIRES_NEW会额外占用连接滥用可能导致连接池耗尽。24. 回滚规则Spring 默认对 RuntimeException 和 Error 回滚对 checked exception 不回滚。Transactional(rollbackForException.class)publicvoidimportUsers(Filefile)throwsIOException{// ...}不要吞异常try{mapper.insert(row);}catch(Exceptione){log.error(failed,e);}吞掉异常会让事务认为执行成功。25. Service 方法设计好的 Service 方法表达业务用例。控制事务。调用一个或多个 Mapper/Repository。做权限和业务校验。不拼 SQL。TransactionalpublicLongenrollCourse(LonguserId,LongcourseId){CourseDOcoursecourseMapper.selectByIdForUpdate(courseId);if(!course.canEnroll()){thrownewBusinessException(课程不可报名);}enrollmentMapper.insert(userId,courseId);courseMapper.increaseLearnerCount(courseId);returncourseId;}26. Mapper 测试数据准备推荐每个测试显式准备数据Sql(/sql/user-fixture.sql)MybatisTestclassUserMapperTest{}或使用 Testcontainers Flyway 初始化。测试应覆盖正常查询。空结果。动态条件组合。插入主键回填。唯一约束冲突。复杂 ResultMap。27. 工程规范补充Mapper 方法命名selectByIdselectPagesearchByConditioninsertupdateSelectivedeleteById业务语义查询selectPublishedCoursesselectUserLearningProgressselectOrderSummary不要query1listgetDataselectMap28. 多模块项目组织knowledge-api knowledge-application knowledge-domain knowledge-infrastructureMyBatis Mapper 可放 infrastructureknowledge-infrastructure/ ├── mapper/ ├── repository/ └── persistence-object/领域层不依赖 MyBatis。29. 工程化专家题补充29.1 为什么事务方法不应包含远程调用远程调用耗时不可控会延长数据库连接和锁持有时间增加死锁和连接池耗尽风险。应尽量在事务外完成远程调用或使用本地消息表、事件、最终一致性方案。29.2 Mapper 测试为什么要覆盖动态 SQL 分支动态 SQL 的错误通常只在特定条件组合下出现如多余and、空集合、缺少 where、参数名错误。覆盖不同分支可以提前发现生产 SQL 错误。29.3 代码生成如何治理生成代码应可重复生成避免手工修改生成文件。业务扩展写在独立 Mapper 或扩展文件中。生成器配置纳入版本管理生成代码和手写代码边界清晰。