MyBatis-Plus 批量插入实践
1. 背景
在使用 MyBatis-Plus(以下简称 MP)的过程中,经常会遇到批量插入数据的场景。
MP 提供了多种批量插入的实现方式,但它们在实现原理、性能表现以及对主键自动填充的支持程度上存在明显差异。
本文结合源码和实际使用经验,对这些方式进行一次系统梳理。
2. 批量插入的实现方式
统一说明:关于 saveBatch 的执行方式
在理解MyBatis-Plus提供的多种批量插入方式之前,需要先澄清一个常见误解
saveBatch 并不等价于“循环执行数据库写操作”。
虽然在实现层面上,saveBatch 会对待插入的数据集合进行遍历,但该遍历仅用于向 JDBC Batch 中追加 SQL 语句,并不会在每次循环时都向数据库发送请求。
其核心机制是:
- 使用
ExecutorType.BATCH - 多次
insert调用仅将 SQL 暂存于 JDBC Batch - 每累计到指定的
batchSize,才会触发一次flushStatements flush只是将 SQL 发送至数据库,事务提交仍由外层事务控制
从执行效果上看:
1 | N 次 insert 调用 |
因此,saveBatch(list, batchSize) 的语义应理解为:
按 batchSize 对数据进行分片(分批)执行 JDBC Batch,而非逐条写入数据库。
这种方式不同于数据库层面的单条多值 SQL(如 insert into values (...), (...)),而是 JDBC 层面的批处理机制
2.1 saveBatch(最常用)
用法
1 |
|
实现原理
- 方法来自
IService - 底层使用
SqlSession,并指定ExecutorType.BATCH - 每累计
batchSize次insert后触发一次flushStatements
简化后的核心逻辑如下:
1 | sqlSession.insert("insert"); |
其本质是 JDBC Batch,而不是数据库层面的单条多值 SQL。
- 官方推荐的批量写入方式
- 支持自动分批,避免一次性加载过多数据导致内存问题
- 实现的是“多条 insert + JDBC batch”
- 对数据库自增主键的回填支持依赖 JDBC 驱动,不稳定
适用场景
- 普通业务批量写入
- 数据量在 1k~5w 之间
2.2 saveOrUpdateBatch(批量 Upsert)
用法
1 | this.saveOrUpdateBatch(list); |
实现原理
- 根据主键是否为空判断是 insert 还是 update
- 内部仍然使用 JDBC batch
- 会包含额外的判断逻辑
- 性能低于
saveBatch - 存在额外的判断和更新开销
- 适合新增与更新混合的场景
2.3 Mapper XML 自定义批量 Insert(单 SQL)
Mapper 方法
1 | int insertBatch( List<User> list); |
XML 实现(MySQL 示例)
1 | <insert id="insertBatch"> |
生成的 SQL 示例
1 | insert into user(name, age) values |
- 单条 SQL,数据库只解析一次,性能最高
- SQL 长度受数据库限制
- 不同数据库语法差异较大,可移植性一般
- MP 无法介入执行过程,主键回填能力缺失
适用场景
- 大批量数据导入
- 对性能要求极高
- 每批数据量可控(通常 1k~5k)
2.4 Db.saveBatch
Db.saveBatch 是什么
Db.saveBatch 是 MyBatis-Plus 提供的静态工具方法,用于在不依赖 Service Bean 的情况下执行批量插入。
1 | Db.saveBatch(list); |
位于 com.baomidou.mybatisplus.extension.toolkit.Db。
与 Service.saveBatch 的区别
| 方式 | 调用方式 |
|---|---|
Service.saveBatch |
userService.saveBatch(list) |
Db.saveBatch |
Db.saveBatch(list) |
Db.saveBatch 采用静态调用方式,不依赖 Spring 注入。
底层实现
两者底层实现几乎一致,最终都会走到:
1 | SqlHelper.executeBatch |
简化源码如下:
1 | public static <T> boolean saveBatch(Collection<T> entityList, int batchSize) { |
优点:
- 不依赖 Service 层
- 适合工具类、脚本、初始化数据等场景
缺点:
- 绕过 Service 层,事务边界不直观
- 在复杂业务中可维护性较差
补充说明:Db.saveBatch 为什么事务支持较弱
为什么事务支持较弱
Db.saveBatch 在功能上与 Service.saveBatch 类似,但在事务管理方面存在一定局限,主要原因在于其调用方式和事务边界不够直观。
1. 静态工具方法,绕过 Service 层
Db.saveBatch 以静态方法形式存在:
1 | Db.saveBatch(list); |
它不依赖具体的 Service Bean,也不会触发 Service 层常见的 AOP 逻辑,例如:
@Transactional- 统一的事务传播策略
- 业务层的拦截与扩展
因此,在阅读代码时,很难通过方法签名判断其是否运行在 Spring 管理的事务上下文中。
2. 内部自行获取 SqlSession
Db.saveBatch 内部通过 SqlHelper.executeBatch 获取并使用 SqlSession。
如果当前线程中不存在已绑定的事务上下文,则会:
- 创建新的
SqlSession - 使用自动提交或独立提交的方式执行 batch
这会导致以下问题:
- 即使外层方法没有事务注解,代码仍然可以正常写入数据库
- 多次
Db.saveBatch调用之间,无法天然形成一个原子事务
3. 对 @Transactional 的依赖更加“隐式”
当 Db.saveBatch 被调用时,是否真正处于事务中,完全取决于调用点:
1 |
|
在这种情况下,只要:
- 方法由 Spring 管理
- 调用未发生在同类内部方法
事务才能生效。
相比之下,Service.saveBatch 通常位于 Service Bean 内部,其调用路径更符合 Spring 事务的设计预期,事务边界也更加清晰。
4. 不利于复杂事务与业务编排
在以下场景中,Db.saveBatch 的事务问题会更加明显:
- 一个方法中包含多次批量写操作
- 批量写操作与其他数据库操作需要处于同一事务
- 需要使用事务传播行为(如
REQUIRES_NEW)
由于 Db.saveBatch 缺乏明确的业务语义和 Service 层边界,这类事务编排往往难以维护和理解。
5. 总结
Db.saveBatch 并非完全不支持事务,而是:
- 事务是否生效依赖调用环境
- 事务边界不直观,容易被误用
- 不适合作为核心业务写路径的主要手段
因此,更推荐在核心业务场景中使用 Service.saveBatch,而将 Db.saveBatch 用于:
- 初始化数据
- 脚本或工具类
- 对事务要求较低的简单批量写入场景
Spring 事务传播策略
Spring 通过事务传播策略(Propagation)来定义一个事务方法在被另一个事务方法调用时的行为方式,即:
当前方法应当加入已有事务,还是创建新的事务。
常见的事务传播策略如下:
| 传播策略 | 行为说明 |
|---|---|
REQUIRED(默认) | 如果当前存在事务,则加入该事务;否则新建一个事务 |
REQUIRES_NEW | 始终新建一个事务,并挂起当前已有事务 |
SUPPORTS | 如果当前存在事务,则加入;否则以非事务方式执行 |
NOT_SUPPORTED | 以非事务方式执行,并挂起当前事务 |
MANDATORY | 必须存在事务,否则抛出异常 |
NEVER | 必须不存在事务,否则抛出异常 |
NESTED | 如果存在事务,则创建嵌套事务(基于保存点) |
其中:
REQUIRED是最常用、也是默认的传播策略REQUIRES_NEW适用于需要独立提交或回滚的场景NESTED依赖数据库对保存点(Savepoint)的支持
事务传播策略通常通过 @Transactional 注解进行配置,例如:
1 |
|
合理使用事务传播策略,可以使业务逻辑中的事务边界更加清晰,有助于提升系统的可维护性和可靠性。
3. 主键自动填充支持度
| 批量方式 | 自增 ID(AUTO_INCREMENT) | 雪花 ID(ASSIGN_ID) | UUID(ASSIGN_UUID) | 备注 |
|---|---|---|---|---|
Service.save |
稳定支持 | 稳定支持 | 稳定支持 | 单条 insert |
Service.saveBatch |
不稳定 | 稳定支持 | 稳定支持 | 自增依赖 JDBC |
Db.saveBatch |
不稳定 | 稳定支持 | 稳定支持 | 同上 |
| XML foreach 单 SQL | 不支持 | 不支持 | 不支持 | MP 无法回填 |
3.1 为什么批量插入下自增主键不稳定
单条 Insert 的情况
1 | insert into user(name) values ('a'); |
- JDBC 可以通过
getGeneratedKeys()获取主键 - MP 可以完成主键回填
Batch Insert 的真实执行方式
1 | ps.addBatch(); |
存在的问题:
- 并非所有 JDBC 驱动都支持返回多条 generated keys
executeBatch()返回结果的结构在不同数据库和驱动中不一致- MyBatis 对批量 generated keys 的支持有限
因此在批量场景下,自增主键回填不可作为可靠依赖。
MySQL 驱动情况说明
| MySQL 驱动版本 | 批量自增回填支持 |
|---|---|
| 较老版本 | 不支持 |
| 新版本 | 部分支持 |
| 复合主键 | 不支持 |
4. 特殊场景:父子表批量插入
在父子表结构中,子表通常需要依赖父表主键作为外键。
核心原则
父表主键必须在插入子表之前就已经确定。
因此,不能依赖数据库自增主键在批量插入后回填。
推荐方案:应用层生成主键
表结构设计
1 |
|
父表与子表统一使用应用层生成的主键策略。
示例流程
1 |
|
- 父子关系在内存中即可确定
- 支持真正的批量插入
- 不依赖数据库回填能力
- 事务边界清晰
不推荐的方案
- 依赖
saveBatch回填自增主键 - 批量插入后通过
max(id)或回查补齐关联关系 - 先插子表,再回填父表 ID
5. 总结
saveBatch/Db.saveBatch本质是 JDBC batch,并非单条多值 SQL- 批量插入场景下,不应依赖数据库自增主键回填
- 推荐统一使用应用层生成主键(如 ASSIGN_ID)
- 父子表批量插入的关键在于:父表主键必须提前确定
- 对性能极致敏感的场景,可考虑 XML 方式并手动处理主键








