1. 项目概述批量保存失效的典型场景最近在项目里做数据迁移和报表生成遇到了一个挺典型的问题用 Mybatis-Plus 的saveBatch()方法批量插入数据日志里看着执行了但数据库里就是没数据或者只插入了部分数据。这问题乍一看有点懵毕竟saveBatch()号称是“开箱即用”的批量操作怎么就不灵了呢其实这背后涉及到 Mybatis-Plus 的批处理机制、事务管理以及一些容易被忽略的配置细节。如果你也正在为saveBatch失效而头疼或者想提前避坑那这篇从实际踩坑中总结的经验应该能给你一些清晰的思路。saveBatch()失效不是一个单一的问题它更像是一个“症状”可能由多种“病因”引起。从最基础的数据库连接配置到事务的传播行为再到实体类字段的映射任何一个环节出问题都可能导致批量保存静默失败。网上很多文章只讲“怎么用”但很少深入讲“为什么失效”以及“失效了怎么查”。接下来我就结合自己的排查过程把这些问题掰开揉碎了讲清楚。2. 核心原理与失效根源深度剖析要解决问题首先得理解saveBatch()是怎么工作的。很多人以为它是一条 SQL 插入多条数据但实际上在默认情况下Mybatis-Plus 的saveBatch()走的是“预处理语句批量执行”的路径而不是真正的INSERT INTO ... VALUES (), (), ()这种单条 SQL 批处理。2.1 Mybatis-Plus 批处理的默认机制默认情况下当你调用saveBatch(collection)时Mybatis-Plus 会这样做获取 SqlSession。为集合中的每个实体对象循环执行sqlSession.insert(“mapper.insert”, entity)。关键在于它依赖于底层的 JDBC 驱动是否支持批处理rewriteBatchedStatements参数以及 SqlSession 的ExecutorType是否被正确设置为BATCH。这里就引出了第一个失效点数据库连接 URL 的配置。以 MySQL 为例如果不在 JDBC URL 后面加上rewriteBatchedStatementstrue这个参数那么即使 Mybatis-Plus 以批处理方式提交JDBC 驱动也会将其拆分成多条独立的 INSERT 语句逐条执行性能提升有限但通常不会导致数据丢失。然而在某些复杂的交互中如果配置不当可能会引发意想不到的问题。2.2 事务管理失效的重灾区第二个也是最常见的失效根源在于事务。saveBatch()方法本身并不开启事务。它通常需要在一个声明式事务例如Transactional的上下文中运行才能保证所有插入操作原子性要么全成功要么全失败。失效场景一非事务上下文调用如果你在一个没有Transactional注解的方法里直接调用saveBatch()那么每条插入语句都可能立即提交取决于数据库的自动提交设置。如果在循环插入过程中间发生了异常已经执行成功的语句对应的数据就永久留存了造成“部分插入”的失效现象。失效场景二事务传播行为不当这是一个高级但常见的坑。考虑下面这个调用链Service public class ServiceA { Autowired private ServiceB serviceB; Transactional public void methodA() { // ... 一些操作 serviceB.saveBatch(dataList); // 调用B服务的方法 // ... 另一些操作如果这里抛出异常 } } Service public class ServiceB { public void saveBatch(ListEntity list) { // 直接调用 mapper 或 service 的 saveBatch myService.saveBatch(list); } }如果ServiceB.saveBatch方法没有Transactional注解那么它会继承methodA的事务。这看起来没问题。但是如果ServiceB.saveBatch内部捕获了异常并处理了没有抛出去那么当methodA后续操作失败回滚时saveBatch插入的数据并不会回滚因为 Mybatis-Plus 的批量操作在事务提交前数据可能已经 flush 到了数据库具体行为与数据库和驱动有关而一个内部被“吞掉”的异常使得事务管理器认为这部分操作是成功的。失效场景三自调用导致事务失效这是 Spring AOP 代理机制的经典问题。如果在一个类内部一个非事务方法调用了同一个类里标注了Transactional的方法事务注解是会失效的。因为Transactional是基于代理实现的自调用不走代理对象。Service public class MyService { public void outerMethod() { // 这里直接调用内部方法事务不生效 this.innerBatchSave(dataList); } Transactional public void innerBatchSave(ListEntity list) { saveBatch(list); } }在这种情况下innerBatchSave上的Transactional形同虚设saveBatch的操作不在一个事务里风险极大。2.3 全局配置与局部设置的影响Mybatis-Plus 提供了一些控制批量行为的配置项如果理解有误也会导致问题。mybatis-plus.global-config.db-config.insert-strategy: 字段插入策略。如果设置为NOT_NULL或NOT_EMPTY而你的实体对象中存在为null或空字符串的字段这些字段将不会被加入 INSERT 语句。如果数据库该字段有非空约束且无默认值就会导致批量插入失败。但更棘手的是如果某条数据的所有有效字段都因为策略被忽略可能会产生一条无意义的插入如只插入主键或直接被跳过造成数据不一致。sqlSessionTemplate的 ExecutorType: 在同时使用 MyBatis 和 Mybatis-Plus或者有自定义 SqlSessionTemplate 的场景下如果 ExecutorType 被设置为SIMPLE默认那么批处理效率会降低但一般不会失效。如果被错误地覆盖或配置可能影响批量执行。2.4 实体类与数据库映射问题这属于基础问题但在批量操作时其影响会被放大。主键冲突如果实体主键是手动赋值非自增且批量数据中存在重复主键会导致唯一约束冲突整个批量操作会失败。但对于自增主键如果数据库表自增机制有问题如步长设置也可能导致意外。字段类型不匹配实体类字段类型与数据库字段类型不兼容在单条插入时可能报错但在批处理中如果某些数据能隐式转换而另一些不能可能导致部分成功部分失败。字段忽略注解TableField(exist false)标注的字段不会参与 SQL 生成这没问题。但如果误用了TableField(insertStrategy FieldStrategy.NEVER)会导致该字段在任何情况下都不插入如果数据库该字段非空且无默认值则插入失败。注意批量操作中的异常处理需要格外小心。默认情况下一条数据的失败可能会让整个批量操作回滚在事务内但也可能只导致当前失败的数据被跳过这取决于具体的数据库驱动和 Mybatis 的配置。务必通过日志或返回值确认批量操作的实际影响范围。3. 问题排查与诊断实战指南当发现saveBatch()失效数据没存进去时不要慌按照以下步骤进行系统性排查可以快速定位问题。3.1 第一步开启最详细的日志日志是排查问题的第一手资料。确保你的应用日志级别能打印出 MyBatis 执行的 SQL 语句和事务信息。在application.yml中配置logging: level: # 查看执行的SQL语句和参数 com.yourmapper.package: debug # 你的Mapper接口所在包 # 查看事务的开启、提交、回滚 org.springframework.jdbc.support.JdbcTransactionManager: debug org.springframework.orm.jpa.JpaTransactionManager: debug # 如果用JPA # 查看Mybatis-Plus的具体操作可选 com.baomidou.mybatisplus: debug开启日志后观察控制台调用saveBatch时是否打印了多条INSERT语句还是一条INSERT ... VALUES (...), (...), ...语句这可以判断批处理是否真正生效。是否有Creating new transaction、Committing transaction、Rolling back transaction等日志这可以判断事务是否正常开启和结束。仔细查看 SQL 语句中插入的字段和值是否与你的实体对象预期一致有没有字段被遗漏检查插入策略值是否正确特别是日期、枚举类型3.2 第二步验证事务边界这是解决“部分插入”或“完全不插入”问题的关键。确认入口方法找到调用saveBatch的那个方法检查它或者它上层调用栈的方法是否标注了Transactional。确保这个注解是加在Service层的公有方法上。检查异常处理仔细审查从Transactional方法入口到saveBatch调用之间的所有代码特别是try-catch块。确保任何可能发生的异常都被抛出到事务切面层而不是在方法内部被“吞掉”。一个常见的错误是捕获了Exception或RuntimeException并只做了日志记录。排查自调用检查是否存在同一个类内非事务方法调用事务方法的情况。可以通过将事务方法移到另一个Service或者通过ApplicationContext获取代理对象再调用来解决。// 在类内部获取代理对象调用不推荐结构混乱 ((MyService) AopContext.currentProxy()).innerBatchSave(list);更推荐的做法是重构代码结构避免自调用。3.3 第三步检查数据库连接与实体配置JDBC URL检查 MySQL 连接 URL 是否包含rewriteBatchedStatementstrue。对于大批量插入这个参数能显著提升性能并确保批处理行为符合预期。jdbc:mysql://localhost:3306/db?useUnicodetruecharacterEncodingutf8useSSLfalserewriteBatchedStatementstrueserverTimezoneAsia/Shanghai实体类审计检查主键策略TableId。如果是ASSIGN_ID雪花算法或AUTO数据库自增一般没问题。如果是INPUT请确保你的集合里没有重复ID。逐一检查每个字段的TableField注解。确认insertStrategy是否符合预期。如果你希望某个字段为空时插入NULL应使用FieldStrategy.DEFAULT或IGNORED而不是NOT_NULL。检查枚举类型字段。如果数据库存的是数字或字符串需要使用EnumValue注解标记实体类中对应的属性并配置 Mybatis-Plus 的枚举处理器否则插入的值可能是枚举对象的name()导致类型不匹配。3.4 第四步进行隔离与最小化测试如果以上步骤都没发现问题问题可能出在更复杂的交互中。尝试创建一个最简化的测试环境写一个全新的、独立的Service方法只做一件事调用saveBatch插入几条简单的测试数据。这个方法加上Transactional。在测试类或一个干净的 Controller 里调用这个方法。观察结果。如果最小化测试成功了说明你的代码和配置本身没问题问题出在原有的业务上下文环境里比如可能和其他数据库操作、锁、中间件产生了交互。如果最小化测试也失败那么问题很可能出在全局配置、数据库本身如触发器、约束或依赖版本上。4. 解决方案与最佳实践根据不同的失效原因这里给出对应的解决方案和编码建议。4.1 确保事务正确配置方案1显式声明事务在执行批量操作的 Service 方法上明确添加Transactional(rollbackFor Exception.class)。rollbackFor Exception.class确保了无论是受检异常还是非受检异常都能触发回滚这是更安全的做法。Service public class DataTransferService { Transactional(rollbackFor Exception.class) public void batchInsertData(ListMyEntity dataList) { // 其他业务逻辑... myEntityService.saveBatch(dataList); // 其他业务逻辑... } }方案2处理复杂事务传播如果批量插入是一个更大事务的一部分且你希望它独立提交比如记录日志即使主事务回滚日志也要保留可以考虑使用REQUIRES_NEW传播级别。但这会创建新事务需要谨慎使用因为可能引发死锁。Service public class LogService { Transactional(propagation Propagation.REQUIRES_NEW) // 开启新事务 public void batchInsertLog(ListLogEntity logs) { logService.saveBatch(logs); } }方案3避免自调用将需要事务的方法拆分到不同的 Service 类中或者使用基于接口的代理确保 Spring AOP 能生效。这是最根本的解决方案。4.2 优化批量保存配置与用法方案1调整批量处理参数Mybatis-Plus 的saveBatch方法有一个重载版本saveBatch(collection, batchSize)可以指定批处理大小。默认值通常是 1000。对于海量数据如10万条一次性放入一个集合调用saveBatch可能会消耗大量内存。更优的做法是进行分片批量处理。public void hugeDataInsert(ListMyEntity hugeList) { int batchSize 1000; // 根据数据库和性能调整 int total hugeList.size(); for (int i 0; i total; i batchSize) { int end Math.min(total, i batchSize); ListMyEntity subList hugeList.subList(i, end); myEntityService.saveBatch(subList, batchSize); // 每批提交后可以稍作清理或记录进度 log.info(“已批量插入 {} 条数据”, end); } }方案2使用自定义注入的 SqlSession在极少数需要对批处理有绝对控制权的场景你可以直接使用 MyBatis 的SqlSession并手动将其执行器类型设置为BATCH。但这需要自行管理 SqlSession 的生命周期和事务复杂度较高一般不推荐。4.3 实体类与数据库的匹配策略统一字段策略在实体类父类或通过全局配置设定合理的字段默认策略。对于新项目建议在全局配置中将insertStrategy和updateStrategy设置为FieldStrategy.DEFAULT然后在具体字段上用TableField注解进行特例化覆盖这样最清晰。mybatis-plus: global-config: db-config: insert-strategy: default update-strategy: default谨慎处理枚举定义枚举时实现IEnum接口或使用EnumValue注解并在配置中启用枚举处理器。// 方式一实现 IEnum 接口 public enum StatusEnum implements IEnumInteger { ENABLE(1, “启用”), DISABLE(0, “禁用”); private final int value; private final String desc; StatusEnum(int value, String desc) { this.value value; this.desc desc; } Override public Integer getValue() { return this.value; } } // 方式二使用 EnumValue 注解 (推荐更灵活) public enum StatusEnum { EnumValue // 标记数据库存储的值是 1 ENABLE(1, “启用”), EnumValue // 标记数据库存储的值是 0 DISABLE(0, “禁用”); EnumValue private final int code; private final String desc; StatusEnum(int code, String desc) { this.code code; this.desc desc; } }在配置文件中启用mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler5. 高级场景与疑难杂症处理除了上述常见问题在一些复杂的集成或特定场景下saveBatch还会遇到更隐蔽的挑战。5.1 多数据源环境下的路由问题在配置了动态数据源如使用DS注解的项目中saveBatch可能因为线程上下文切换导致数据源路由失效。例如你在一个标记了DS(“slave”)的方法里准备数据然后调用一个DS(“master”)的 Service 方法进行saveBatch。如果数据源切换依赖于线程上下文如 ThreadLocal而在批量处理过程中可能使用了多线程或异步上下文丢失就会导致插入操作执行在错误的数据源上。解决方案确保事务和DS注解在同一线程将DS注解和Transactional注解放在同一个 Service 方法上确保从开始到结束数据源是明确的。避免在批处理内部切换数据源如果批量操作涉及多个库考虑将其拆分成多个独立的批量操作每个操作都有明确的数据源上下文。使用编程式数据源路由在复杂场景下放弃注解在代码中通过DataSourceContextHolder等工具手动设置和清除数据源键确保在批处理的整个生命周期内上下文稳定。5.2 与数据库特定功能的冲突某些数据库特性或表设计可能与 Mybatis-Plus 的批量插入逻辑产生冲突。触发器Trigger如果目标表上有BEFORE INSERT触发器并且触发器逻辑复杂或出错会导致整批插入失败。需要检查数据库触发器日志。唯一索引/约束冲突这是最直接的错误。saveBatch在遇到第一条违反唯一约束的数据时就会抛出异常如DuplicateKeyException导致事务回滚。对于需要“忽略重复”或“更新重复”的场景saveBatch无能为力。此时需要考虑使用数据库特有的语法如 MySQL 的INSERT ... ON DUPLICATE KEY UPDATE ...这需要通过自定义 Mapper XML 文件来实现。自增主键批次预分配问题在一些数据库如早期版本的 PostgreSQL 使用SERIAL或特定配置下批量插入时自增 ID 的获取可能不如预期。Mybatis-Plus 的雪花算法ASSIGN_ID可以很好地避免这个问题。5.3 性能瓶颈分析与调优当数据量极大时即使saveBatch正常工作也可能遇到性能瓶颈。批处理大小batchSizebatchSize不是越大越好。需要根据数据库的max_allowed_packet配置和网络往返开销来权衡。通常 500-2000 是一个合理的范围。可以通过压测找到最适合当前环境的数值。JDBC 参数除了rewriteBatchedStatements还可以考虑useServerPrepStmts、cachePrepStmts等参数来优化预处理语句。关闭自动提交确保你的操作在一个事务内这本身就关闭了自动提交是批处理的前提。分批提交与内存管理如前所述对于超大数据集一定要在应用层做分片。每处理完一批可以尝试清空子列表的引用提示垃圾回收器。for (int i 0; i total; i batchSize) { ListEntity batchList hugeList.subList(i, Math.min(i batchSize, total)); // 为了GC友好可以复制一份但会消耗额外内存和CPU // ListEntity batchList new ArrayList(hugeList.subList(i, end)); service.saveBatch(batchList); batchList null; // 帮助GC }考虑替代方案在极端性能要求下可以绕过 ORM 框架直接使用 JDBC 的addBatch()和executeBatch()或者使用数据库原生的批量导入工具如 MySQL 的LOAD DATA INFILE。Mybatis-Plus 的saveBatch在易用性和性能之间取得了很好的平衡但并非最快的方案。排查Mybatis-Plus saveBatch() 批量保存失效的问题是一个从表象深入底层的过程。它考验的是你对框架机制、Spring 事务管理、数据库连接以及自身业务代码的综合理解。最有效的办法永远是结合详细的日志构建最小复现场景然后对照本文提到的几个方面——事务、配置、实体、上下文——进行逐一验证。记住批量操作无小事数据一致性是关键上线前充分的测试是必不可少的环节。