✅ 一、核心概念
概念 |
说明 |
ISqlInjector |
MyBatis-Plus 扩展点,用于在 启动阶段 把自定义 SQL 方法注入到 每个 Mapper 接口 的 MappedStatement 中,从而像 selectById 一样直接调用。 |
AbstractMethod |
自定义 SQL 的模板类——重写 injectMappedStatement 来生成 SQL。 |
DefaultSqlInjector |
MP 默认实现;自定义时只需继承它并追加新方法。 |
使用场景 |
真批量插入、逻辑删除增强、多表 join、数据权限、租户过滤等。 |
✅ 二、操作步骤(5 步即可跑通)
✅ 步骤1:新建自定义方法类
public class InsertBatchSomeColumn extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(
Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 1. SQL 模板
String sql = "<script>INSERT INTO %s (%s) VALUES " +
"<foreach collection=\"list\" item=\"item\" separator=\",\">" +
"(<foreach collection=\"columns\" item=\"col\" separator=\",\">" +
"#{item.${col.property}}" +
"</foreach>)" +
"</foreach>" +
"</script>";
// 2. 动态列
List<TableFieldInfo> columns = tableInfo.getFieldList()
.stream()
.filter(f -> !f.isLogicDelete())
.collect(Collectors.toList());
String columnNames = columns.stream()
.map(TableFieldInfo::getColumn)
.collect(Collectors.joining(","));
sql = String.format(sql, tableInfo.getTableName(), columnNames);
// 3. 构建 SqlSource
SqlSource sqlSource = languageDriver.createSqlSource(
configuration, sql, modelClass);
// 4. 返回 MappedStatement(注意 id 与方法名一致)
return addInsertMappedStatement(mapperClass, modelClass,
"insertBatchSomeColumn", sqlSource, new NoKeyGenerator(), null, null);
}
}
✅ 步骤2:自定义 SQL 注入器
@Component
public class CustomSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
// 保留父类所有方法
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
// 追加自定义方法
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
}
✅ 步骤3:注册注入器(Spring Boot 场景)
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
return new MybatisPlusInterceptor(); // 已有其他插件可继续添加
}
// 注入器已在 @Component 中自动注册,无需额外配置
}
✅ 步骤4:扩展 BaseMapper
public interface MyBaseMapper<T> extends BaseMapper<T> {
/**
* 真批量插入
*/
int insertBatchSomeColumn(@Param("list") List<T> list);
}
✅ 步骤5:业务 Mapper 继承并使用
public interface UserMapper extends MyBaseMapper<User> { }
// 使用示例
List<User> users = buildUsers(20000);
int rows = userMapper.insertBatchSomeColumn(users);
✅ 三、常见错误与解决方案
错误 |
原因 |
解决 |
Invalid bound statement (not found) |
方法名与 MappedStatement id 不一致 |
保证 injectMappedStatement 中 addInsertMappedStatement 的 id 与 Mapper 方法名完全一致 |
MybatisPlusException: SQL injection failure |
SQL 拼写错误或列不匹配 |
打印 sql 变量,用日志确认 |
打包后 Lambda 失效 |
低版本 MyBatis-Plus 与 Spring Boot 分离打包导致 |
升级到 3.5.x 或避免 Lambda 方式写 SQL |
数据量过大,SQL 报错 |
MySQL 单条 SQL 长度限制 |
分片批处理,每批 1000 条 |
✅ 四、注意事项
类别 |
说明 |
事务 |
大批量插入必须加 @Transactional |
主键策略 |
自增主键需设置 @TableId(type = IdType.AUTO) ,雪花 ID 无需额外处理 |
字段过滤 |
使用 tableInfo.getFieldList().filter(...) 排除逻辑删除、乐观锁字段 |
分页批处理 |
一次性 > 10 万时,用 ListUtils.partition(list, 1000) 分批提交 |
SQL 大小 |
MySQL 默认最大 16M;可通过 max_allowed_packet 参数调整 |
✅ 五、使用技巧 & 高级玩法
技巧 |
代码示例 |
多租户字段自动注入 |
在 AbstractMethod 中读取 TenantLineInnerInterceptor 的租户 ID,拼接 SQL |
动态表名 |
使用 tableInfo.getTableName() + 租户后缀:String table = tableInfo.getTableName() + "_" + tenantId; |
逻辑删除过滤 |
重写 InsertBatchSomeColumn ,排除 tableInfo.isLogicDelete() 字段 |
性能监控 |
结合 p6spy 或 MybatisPlusSqlLog 打印耗时 |
✅ 六、最佳实践与性能优化
实践 |
说明 |
rewriteBatchedStatements=true |
JDBC URL 必加,MySQL 驱动将多条 insert 合并成一条多值 insert |
分批大小 |
1000 条/批;SSD 高并发环境可 2000~5000 |
并行插入 |
CompletableFuture + 分片,多个线程独立事务,加速 3~5 倍 |
索引优化 |
批量插入前关闭二级索引 → 插入后重建,可再提升 30% 性能 |
日志级别 |
生产环境关闭 SQL 日志,开发环境开启 |
✅ 七、一句话总结
自定义 SQL 注入器 = 在启动阶段“写死”通用 SQL,让全局 Mapper 都拥有“真批量”能力;只需继承 DefaultSqlInjector
、实现 AbstractMethod
,一分钟即可在业务中调用高性能批量插入,彻底告别“伪批量”。