MybatisPlus批量插入saveBatch的隐藏‘坑’:字段为null竟然会让rewriteBatchedStatements失效?
MybatisPlus批量插入性能陷阱:字段null值如何让rewriteBatchedStatements失效
当你信心满满地在spring.datasource.url后追加了rewriteBatchedStatements=true参数,却发现saveBatch()的批量插入性能依然像蜗牛爬行——这很可能是因为实体类中那些不起眼的null字段在暗中作祟。本文将揭示MybatisPlus批量操作中这个容易被忽视的性能杀手,并给出从注解配置到源码层面的完整解决方案。
1. 为什么rewriteBatchedStatements会神秘失效
许多开发者在配置完rewriteBatchedStatements=true后,会想当然地认为MybatisPlus会自动将saveBatch()转换为高效的批量SQL语句。但当你用JDBC日志或性能监控工具观察时,可能会震惊地发现系统仍在执行大量单条INSERT语句。
核心症结在于MybatisPlus对批处理语句的组装逻辑:当实体对象中存在任何null字段时(即使数据库允许该字段为null),MybatisPlus会保守地将整个批量操作退化为单条循环插入。这种设计源于JDBC批处理的一个底层约束——批处理要求所有参数类型必须明确。
考虑以下典型场景:
List<User> userList = Arrays.asList( new User(null, "张三", 25), // id为null new User(null, "李四", 30) ); userService.saveBatch(userList);即使你正确配置了rewriteBatchedStatements,由于id字段为null,实际执行的将是:
INSERT INTO user (name, age) VALUES ('张三', 25); INSERT INTO user (name, age) VALUES ('李四', 30);2. 字段处理策略深度解析
要真正发挥批量插入的威力,我们需要精细控制每个字段的生成策略。MybatisPlus提供了多种注解来控制字段行为:
2.1 @TableField的insertStrategy
这是解决null字段问题的关键注解,其策略选项包括:
| 策略值 | 作用 | 适用场景 |
|---|---|---|
| NOT_NULL | 非null才插入 | 必填业务字段 |
| NOT_EMPTY | 非空才插入(字符串) | 非空字符串校验 |
| IGNORED | 始终插入(允许null) | 可选字段 |
| NEVER | 永不插入 | 只读字段 |
典型配置示例:
@TableField(insertStrategy = FieldStrategy.IGNORED) private String optionalField;2.2 自动填充字段的最佳实践
对于创建时间、更新人等系统字段,推荐使用@TableField(fill)实现自动填充:
public class MetaFillHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); this.strictInsertFill(metaObject, "createUser", String.class, getCurrentUser()); } } @Entity public class Order { @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private String updateUser; }3. 确保批量生效的完整解决方案
3.1 实体类配置检查清单
主键策略:明确指定自增或其它生成方式
@TableId(type = IdType.AUTO) private Long id;可选字段处理:对允许为null的业务字段设置
IGNORED策略@TableField(insertStrategy = FieldStrategy.IGNORED) private String remark;系统字段自动填充:创建时间、操作人等字段配置自动填充
3.2 数据准备阶段的防御性编程
即使注解配置正确,数据准备不当仍会导致批量失效。推荐以下实践:
// 错误示例:集合中混入部分字段为null的对象 List<User> users = queryUsers(); userService.saveBatch(users); // 风险点 // 正确做法:确保集合中所有对象关键字段非null List<User> safeUsers = users.stream() .peek(user -> { if(user.getName() == null) { user.setName(""); // 或其它默认值 } }) .collect(Collectors.toList());3.3 性能验证方法
确认批量是否真正生效的三种方式:
日志分析:开启JDBC日志查看实际SQL
logging.level.org.springframework.jdbc=DEBUG性能对比:批量与单条插入的时间差异
源码断点:在
MybatisPlus的SqlHelper类中观察SQL组装过程
4. 高级场景与疑难排查
4.1 动态表名下的批量插入
当使用动态表名时,批量插入需要特殊处理:
// 设置动态表名处理器 DynamicTableNameParser dynamicTableNameParser = new DynamicTableNameParser(); dynamicTableNameParser.setTableNameHandler((sql, tableName) -> { return "actual_table_" + getMonthSuffix(); }); // 批量插入需确保同一批次表名一致 List<Log> logs = generateLogs(); logs.forEach(log -> log.setTableSuffix(getCurrentSuffix())); logService.saveBatch(logs);4.2 大批量数据的分片处理
对于超大规模数据(如10万+),即使使用批量也需分片:
// 使用Guava工具类进行分片 List<List<User>> partitions = Lists.partition(hugeList, 1000); partitions.forEach(partition -> { userService.saveBatch(partition); // 每批提交后短暂休眠,避免数据库压力过大 Thread.sleep(100); });4.3 与事务管理的协同
批量操作与事务的交互需要注意:
提示:Spring默认事务超时可能不适用于大批量操作,建议针对性地调整事务属性
@Transactional(timeout = 30) // 适当延长超时时间 public void importLargeData(List<Data> dataList) { dataService.saveBatch(dataList); }5. 从源码看MybatisPlus的批量决策逻辑
理解SqlHelper类的关键逻辑有助于深度排查问题:
// 伪代码展示批处理决策逻辑 public static boolean determineBatchMode(List<?> list) { for (Object entity : list) { if (hasNullField(entity)) { // 检查任何字段是否为null return false; // 发现null字段,退化为单条模式 } } return canUseRewriteBatchedStatements(); // 检查JDBC配置 }这个实现解释了为什么即使只有一个字段为null,也会导致整个批量操作降级。在实际项目中,可以通过继承MybatisPlus的默认实现来修改这一行为(需谨慎评估影响)。
