MyBatis 如何进行批处理?批量插入如何回填主键?
简化版
MyBatis 可用 ExecutorType.BATCH 将多次相同形态的更新交给 JDBC 批量执行,也可用 <foreach> 生成一条多 values INSERT。BATCH 必须在适当批次调用 flushStatements 或提交,并根据 JDBC 驱动支持用 useGeneratedKeys、keyProperty 回填自增主键;大批量要分片,不能一次堆满内存或超过数据库参数限制。
详细版
BatchExecutor 会复用匹配的 MappedStatement 和 SQL 对应 Statement,反复 addBatch,flush 时才执行并返回 BatchResult/updateCounts;因此单次 mapper update 的返回值不能当作最终影响行数。动态 SQL 生成不同 SQL 时可能拆成多个批次。
useGeneratedKeys="true" keyProperty="id" 使用 JDBC getGeneratedKeys(),依赖数据库与驱动能力;不支持时可用 <selectKey order="BEFORE|AFTER">。批量多对象能否按顺序完整回填必须用目标驱动验证,不能从单行插入行为推断。
完整版教学
一、两种常见批量方式
方式一是循环调用同一 Mapper 方法,但 SqlSession 使用 BATCH Executor:
try (SqlSession session = factory.openSession(ExecutorType.BATCH, false)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
session.flushStatements();
session.commit();
}
方式二是 foreach 生成 INSERT ... VALUES (...), (...)。前者使用 JDBC batch,后者是一条多行 SQL;驱动优化、参数上限、错误定位和主键返回行为都不同。
| 方式 | SQL 形态 | 优点 | 主要风险 |
|---|---|---|---|
ExecutorType.BATCH | 多次调用同一 mapped statement,JDBC addBatch | 单条 SQL 模板稳定,适合复用 Mapper 方法 | flush 前不真正知道结果,异常和主键回填依赖驱动 |
<foreach> 多 values | 一条 INSERT INTO t(...) VALUES (...), (...), ... | 网络往返少,SQL 直观 | SQL 过长、参数上限、单条失败定位困难 |
| 数据库导入工具 | LOAD DATA、copy 等原生命令 | 海量导入吞吐高 | 脱离普通 ORM 流程,需要单独权限与校验 |
记忆钩子:BATCH 是“很多小动作先攒着,flush 时一起发”;foreach 多 values 是“直接拼成一条大 SQL”。两者都叫批量,但执行语义并不一样。
二、为什么要显式 flush
BatchExecutor 的 update 主要把参数加入批次,数据库可能尚未真正执行。flushStatements() 或 commit 才触发 executeBatch,最终影响行数和批次异常也可能到此时才出现。
批次过大时,参数对象和 JDBC 驱动缓冲会占用大量内存。应每几百或几千条分片 flush,具体大小通过数据库参数限制、网络包、行大小和压测确定。
假设导入 10000 行,每行 8 个字段:
方案 A:10000 行一次性攒到最后 flush
- 参数对象、JDBC batch 缓冲、事务日志压力集中
- 第 9000 行失败时回滚和定位成本很高
方案 B:每 500 行 flush 一次
- 约 20 个批次,内存峰值更低
- 日志和异常定位更可控
这里的 500 不是固定标准,而是告诉面试官你理解“批量大小要压测确定”。如果单行字段很多、包含大文本或数据库参数上限较低,批次要更小;如果网络延迟高且行很小,可以适当放大。
三、事务与错误处理
flush 只是把语句发送给数据库,不等于事务已经提交;事务型数据库中后续 rollback 仍可回滚未提交修改。批处理中某条失败时,驱动可能返回部分 updateCounts 或抛 BatchUpdateException,能否精确定位失败行取决于驱动。
Spring 中使用 Batch SqlSessionTemplate 时,要确保它参与正确事务。已有事务已经绑定另一种 ExecutorType 时,不能随意在中途切换执行器;更稳妥的是使用专门批处理边界和模板。
四、自增主键回填
<insert id="insert"
useGeneratedKeys="true"
keyProperty="id"
keyColumn="id">
INSERT INTO users(name) VALUES(#{name})
</insert>
执行后 MyBatis 从 JDBC 读取生成键并写回参数对象的 id。keyColumn 在列顺序或数据库要求明确时使用;复合键可配置多个属性与列。
五、selectKey 的使用边界
数据库使用序列时,可在插入前执行 selectKey 得到主键,再把它带入 INSERT;某些场景则在插入后查询生成值。BEFORE/AFTER 必须符合数据库生成机制。
selectKey 是额外 SQL,不应在不了解并发和序列语义时用“查最大 ID + 1”模拟主键,这会产生竞态。
六、性能不只看 SQL 次数
批量越大不一定越快。还要考虑事务日志、锁持有时间、复制延迟、失败重试成本和连接池占用。海量导入可使用数据库原生导入工具,未必适合通过 ORM 循环完成。
批处理完成后应记录实际成功数量,并对重复执行建立幂等键或唯一约束。
七、常见误区与追问
- 误区:调用 Mapper 的 insert 返回 1 就表示数据库已经执行成功。 BATCH 模式下很多更新只是加入 JDBC batch,真正执行和异常可能到
flushStatements()或提交时才发生。 - 误区:批量越大性能一定越好。 批次过大会放大内存、事务日志、锁持有时间和失败回滚成本,必须按行大小、参数上限和压测结果分片。
- 追问:BATCH 和 foreach 多 values 的核心区别是什么? BATCH 是多次参数绑定后交给 JDBC
executeBatch,foreach 是生成一条大 SQL,驱动行为、SQL 长度和错误定位都不同。 - 追问:自增主键批量回填为什么要验证驱动? MyBatis 依赖 JDBC
getGeneratedKeys(),不同数据库和驱动对批量返回键的顺序、数量支持并不完全一致。 - 误区:flush 就等于 commit。 flush 只是把批次发送执行,事务未提交前仍可回滚,提交边界要单独控制。
- 追问:
selectKey能不能用查最大 ID 加 1? 不能,这在并发下有竞态,应该用数据库序列、自增列或可靠 ID 生成器。
八、加强记忆
BatchExecutor 是“多次 addBatch,flush/commit 时执行”,foreach 多 values 则是“一条大 SQL”。最终行数和异常要到 flush 才明确,主键回填依赖 JDBC 驱动;批量必须分片、处于清晰事务边界,并用目标数据库验证生成键行为。