目录
前言
在Spring Cloud项目中使用了MyBatis-Plus的BaseMapper,在执行mp自带的insert方法时,速度非常慢,发现即使传入一个list,其底层是通过循环遍历list依次插入的,并没有实现mysql支持的insert 多个 value连接的批量插入方式。
同时我还需要自定义创建一些公共方法,所以选择创建一个自定义的MyBaseMapper,增加批量插入、截断表等方法。
环境版本
- idea:IntelliJ IDEA 2019.3.4 (Ultimate Edition)
- Java:1.8
- mysql:5.7.33
- mysql-connector-j:9.3.0
- spring-boot-starter:2.7.18
- mybatis-plus-boot-starter:3.5.12
一、BaseMapper 伪批量插入
在使用BaseMapper的insert方法时,传入了一个List列表(3.5.7新增),但是刷新数据库,看到数据库的数量是一点点增长的,数据量一直都不是整数,所以就猜测MyBatis-Plus的insert是伪批量插入,下面一起查看一下源代码
1. 测试代码
@SpringBootTest
class UserTests {
@Autowired
UserMapper userMapper;
@Test
void test1() {
List<User> userList = new ArrayList<>();
userMapper.insert(userList);
}
}
由于后面都是源码,用截图更方便一点,按住ctrl点击insert进行跳转

2. BaseMapper
可以看到默认批处理大小为1000,通过重载到下面的全参数方法上,这是一种常见的设置默认值的思路,接下来通过一个批处理工具类执行,继续点击下面方法的execute

3. MybatisBatchUtils
这里创建了一个批处理对象,又补充了一些执行参数,继续点击execute

4. MybatisBatch
终于到了执行方法了,重载到完整参数方法上,可以看到有两层for循环,第一层是分批后的结果列表,第二层是每一批内部进行遍历,取出每个对象后调用sqlSession.update方法,一批数据flush一次

二、IService 批量插入
那你可能会想到,mp的IService中也提供了批量插入方法,还是明确的叫saveBatch方法,那么它的底层是不是真批量插入呢,先测试一下看看日志
1. 测试代码
@Autowired
IUserService userService;
@Test
void test4() {
List<User> userList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
User user = new User();
user.setName("姓名"+i);
userList.add(user);
}
userService.saveBatch(userList,2);
}
【沉浸式解决问题】baseMapper can not be null
这里遇到两个新的问题,解决如下:
【沉浸式解决问题】Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required
开启mybatis-plus日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
可以看到虽然5条数据分了3批,但每批都使用的预处理语句,参数依次传输,正常就是依次执行

再带大家看一下源码,按住ctrl点击saveBatch方法

2. IService
IService继承了IRepository,添加默认批处理大小1000,代入IRepository中的重载方法

3. IRepository
这是父接口了,再往下看要找它的实现方法,点击方法左边的绿色箭头

4. CrudRepository
添加一个sql语句函数,再代入它继承的AbstractRepository类中的批处理执行器

5. AbstractRepository
再传递给SqlHelper的批处理执行器

6. SqlHelper
外面一层for循环遍历列表,内部if判断当达到批处理大小时就flush一批数据,具体就是交给mybatis通过预处理语句去实现了,后面再更新一下批处理大小,用于调整最后一批。

三、JDBC源码追踪
最开始找到了mp提供的sql注入器,可以在自定义BaseMapper中注入自定义方法;
又尝试了创建一个 MyBaseMapper.xml,在里面实现自定义方法;
最后找到了通过jdbc参数实现真正批量;
这里为了更有逻辑把顺序反过来讲解。
这一步其实刚进入mybatis,还没有真正发给jdbc进行执行,中间还有很多过程,有没有可能再中间把它转换成真正的批处理呢,像insert value这种形式,让我们继续深入。
紧接着上面的代码调用,继续往下看,点击上一步中sqlSession的flushStatements()方法,开始跟踪
1. SqlSession
SqlSession是sql会话的接口类,点击方法左边的箭头显示实现方法,有三个实现类,一个是默认的,一个是管理的,一个是模板,猜也能猜出来是第一个DefaultSqlSession

那如果不知道是哪个怎么办呢,正常可以通过查看接口注入的地方,但是框架一般比较复杂,不太容易一下找到具体实现位置,我们用一个简单的办法,通过断点来判断,下面演示一下
2. DefaultSqlSession
在方法的第一行打上断点

在测试方法左边选择Debug

可以看到成功的停在断点处了,说明我们走的确实是这个实现类

会话调用执行器,继续点击执行器的flushStatements()方法

3. Executor
执行器同样先是一个接口类,点击方法左侧,又是两个实现类,一个是基础,一个是缓存,不用看,肯定是base,也可以通过断点的方法验证

4. BaseExecutor
进入BaseExecutor,第一个方法代入一个回滚参数,传入重载方法,再次点击doFlushStatements

还在BaseExecutor类中,doFlushStatements是一个抽象方法,有四个子类都实现了它,根据意思判断应该就是第一个批处理执行器,但是这个就没那么肯定了

5. BatchExecutor
打断点验证一下,没什么问题。这里经过一些判断,最核心的是stmt.executeBatch(),而stmt是jdbc的Statement对象,点击executeBatch()方法

6. Statement
进入Statement接口就正式从mybaits到了jdbc的部分了,大家最开始从0学Java的时候应该都写过用jdbc访问数据库
点击左边的箭头查看实现类,这里就没办法直接判断了,给每个类都打断点后发现走的是ProxyStatement,一个代理类,点击它

7. ProxyStatement
进入代理类后无法继续追踪了,点击方法中的delegate.executeBatch()会回到Statement接口,
点击左边查看子类,三个子类都不进入,只能通过debug查看了

打上断点,直接执行到这一步

按F7或者点击进入方法,同时在这里可以看到,代理走的是Hikar连接池的预处理语句,也就是刚刚三个子类中的第二个

暂停查看一下HikariProxyPreparedStatement类,打上断点后提示Line numbers info is not available,应该是行序号不匹配的原因,点击右上角下载资源也不行

8. StatementImpl
刚刚按F7或者点击进入方法,来到了sql语句的实现类
继续点击debug窗口的Step Into,选择进入第二个方法

9. ClientPreparedStatement
来到了预处理语句的客户端,先贴个executeBatchInternal()源代码展示一下
@Override
protected long[] executeBatchInternal() throws SQLException {
Lock connectionLock = checkClosed().getConnectionLock();
connectionLock.lock();
try {
TelemetrySpan span = getSession().getTelemetryHandler().startSpan(TelemetrySpanName.STMT_EXECUTE_BATCH_PREPARED);
try (TelemetryScope scope = span.makeCurrent()) {
span.setAttribute(TelemetryAttribute.DB_NAME, this::getCurrentDatabase);
span.setAttribute(TelemetryAttribute.DB_OPERATION, TelemetryAttribute.OPERATION_BATCH);
span.setAttribute(TelemetryAttribute.DB_STATEMENT, TelemetryAttribute.OPERATION_BATCH);
span.setAttribute(TelemetryAttribute.DB_SYSTEM, TelemetryAttribute.DB_SYSTEM_DEFAULT);
span.setAttribute(TelemetryAttribute.DB_USER, () -> this.connection.getUser());
span.setAttribute(TelemetryAttribute.THREAD_ID, () -> Thread.currentThread().getId());
span.setAttribute(TelemetryAttribute.THREAD_NAME, () -> Thread.currentThread().getName());
if (this.connection.isReadOnly()) {
throw new SQLException(Messages.getString("PreparedStatement.25") + Messages.getString("PreparedStatement.26"),
MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT);
}
if (this.query.getBatchedArgs() == null || this.query.getBatchedArgs().size() == 0) {
return new long[0];
}
// we timeout the entire batch, not individual statements
long batchTimeout = getTimeoutInMillis();
setTimeoutInMillis(0);
resetCancelledState();
try {
statementBegins();
clearWarnings();
if (!this.batchHasPlainStatements && this.rewriteBatchedStatements.getValue()) {
if (getQueryInfo().isRewritableWithMultiValuesClause()) {
return executeBatchWithMultiValuesClause(batchTimeout);
}
if (!this.batchHasPlainStatements && this.query.getBatchedArgs() != null
&& this.query.getBatchedArgs().size() > 3 /* cost of option setting rt-wise */) {
return executePreparedBatchAsMultiStatement(batchTimeout);
}
}
return executeBatchSerially(batchTimeout);
} finally {
this.query.getStatementExecuting().set(false);
setTimeoutInMillis(batchTimeout);
clearBatch();
}
} catch (Throwable t) {
span.setError(t);
throw t;
} finally {
span.end();
}
} finally {
connectionLock.unlock();
}
}
前面都是一些参数设置和判断,我们直接找到最中心的执行部分
一共有三个return,分不同情况继续调用执行方法
- multi-values:批量插入,调用
executeBatchWithMultiValuesClause - multi-statement:多语句批处理,调用
executePreparedBatchAsMultiStatement - one-by-one:串行批处理,调用
executeBatchSerially
找了一篇对这三种方法进行详解的文章,想要了解的点击查看:
MySQL JDBC 实战: PreparedStatement rewriteBatchedStatements 实现原理

到这里我们就可以发现,JDBC实际上是提供了真正的批量插入的,通过batchHasPlainStatements参数和rewriteBatchedStatements进行判断,上图可以看到,正常就是false,前面有个感叹号非运算之后就是true,就只剩下rewriteBatchedStatements参数了,就是重写批处理语句的意思,查阅得知这个是在url中进行设置即可
这里点击下一步就跳到了串行批处理

四、JDBC 批量插入
那么我们就可以通过jdbc实现真批量插入了,由于
BaseMapper.insert(Collection<T> entityList)和IService.(Collection<T> entityList)都是调用了mybatis的SqlSession.flushStatements(),所以设置了这个参数以后两个方法就都实现真正的批量插入了,当然也包括我们直接调用jdbc的预处理语句。
在yml中修改url,添加&rewriteBatchedStatements=true
spring:
# 数据库信息
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/ybws_test?useSSL=false&rewriteBatchedStatements=true
username: root
password: root
注意事项:
- 事务一致性:批量操作仍受事务管理,失败时整体回滚。
- SQL长度限制:合并后SQL可能超max_allowed_packet,需调整该参数。
都调试到这里了,索性继续深入,看看JDBC内部是怎么实现重写的
1. ClientPreparedStatement
1.1 executeBatchInternal() 420行
修改url后取消前面的断点,debug执行可以看到进入了重写语句的return

1.2 executeBatchWithMultiValuesClause() 687行
点击Step Into进入批量处理方法,翻译一下方法注释,可以看到除了批量insert插入还可以批量replace更新
Rewrites the already prepared statement into a multi-values clause INSERT/REPLACE statement and executes the entire batch using this new statement
将已准备好的语句重写为多值子句INSERT/REPLACE语句,并使用此新语句执行整个批处理。
往下看,先是创建了一个批处理语句对象

然后对它初始化,传入批处理大小

把这个变量添加到Watches区域,点击右上角的View,可以看到已经处理成空白语句,参数位置有默认的1000个括号

再往下就是重写部分了,先循环1000个参数
- 判断是否到达一批1000,达到就执行一批,粗略的看感觉一般用不上,i是从0开始的,不是特殊情况不会到达最大值
- 没有到达就一个个替换参数

我们往下执行进入setOneBatchedParameterSet方法查看一下

1.3 setOneBatchedParameterSet() 647行
进入后执行一步,创建一个BindValue[]数组,用来存放一行数据各个字段对应的值,下面的for循环对各个字段依次进行赋值

查看变量区域,由于我们就测试了一个字段,所以只有一个姓名0

1.2 executeBatchWithMultiValuesClause() 687行
往下执行返回到上一步的方法,再次查看batchedStatement,可以看到已经合并了第一个

取消for循环的断点,快进到for循环之后,一批参数都完成合并

再次查看batchedStatement,可以看到已经变成了insert values的格式,此时再执行就是真正的批量插入了

重写的代码就看完啦,往后再看一步,F7继续往下走
1.4 executeLargeUpdate() 1449行
继续F7进入

1.5 executeUpdateInternal() 1094行
在重载方法处打上断点,F9快进到这里,然后F7选择executeUpdateInternal

1.6 executeUpdateInternal() 1122行
前面设置了很多东西,我们在执行处打上断点,F9快进到这里,然后F7进入

1.7 ResultSetInternalMethods() 942行
找到执行的地方打上断点,F9快进到这里,然后F7进入execSQL

2. NativeSession
接下来就是和mysql关系不大了,再往深就是通信部分了,我们找到核心语句
调用this.protocol.sendQueryPacket发送查询数据包,里面使用BufferedOutputStream输出数据流,感兴趣的可以自行深入一下

这里额外提一句,protocol是协议,用的什么协议呢,翻到NativeSession类的最上面
没错,正是Socket协议,JDBC的底层就是使用Socket进行连接数据库的,所以后面就是使用OutputStream,再通过TCP发送到MySQL的server,有时间写一篇JDBC通信解析。

【TODO】JDBC底层通信原理解析
五、MyBaseMapper.xml
通过JDBC实现批量插入的探索就告一段落了,接下来回到正题,除了批量插入还有很多别的公共方法,比如最简单的截断表,所以想尝试下能否通过创建
MyBaseMapper.class和MyBaseMapper.xml实现,在MyBaseMapper.xml中编写自定义方法。
1. 方案1
1.1 MyBaseMapper.class
先创建个自定义的BaseMapper,继承Mybatis-Plus的BaseMapper,添加一个截断方法
package com.baomidou.base;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface MyBaseMapper<T> extends BaseMapper<T> {
// 截断表
void truncateTable();
}
1.2 MyBaseMapper.xml
无论是kimi还是deepseek,都提到了可以动态获取表名,MyBatis-Plus会自动获取实体类上的注解并进行替换,只不过我没找到这一说法的来源。。。。。。

先这么写试试
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baomidou.base.MyBaseMapper">
<!-- 清空表-->
<update id="truncateTable">
TRUNCATE TABLE ${tableName}
</update>
</mapper>
1.3 User.class
package com.baomidou.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField("name")
private String name;
}
1.4 UserMapper.class
package com.baomidou.mapper;
import com.baomidou.base.MyBaseMapper;
import com.baomidou.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends MyBaseMapper<User> {
}
1.5 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baomidou.mapper.UserMapper">
</mapper>
1.6 DemoApplication.class
package com.baomidou;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
}
1.7 UserTest.class
package com.baomidou;
import com.baomidou.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class UserTest {
@Autowired
UserMapper userMapper;
@Test
void test1() {
userMapper.truncateTable();
}
}
运行test1()后报错sql语句不正确,可以看到${tableName}并没有自动替换,也就是说整体逻辑是可行的,只是不能自动替换表名,那就没法做公共方法

多次尝试后,找到一种解决方案
2. 方案2
缺省的部分和方案1相同
2.1 MyBaseMapper.class
添加一个参数传递实体类
package com.baomidou.base;
import com.baomidou.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
public interface MyBaseMapper<T> extends BaseMapper<T> {
// 截断表
void truncateTable(@Param("entityClass") Class<T> entityClass);
}
2.2 MyBaseMapper.xml
通过参数获取实体类,通过mp的方法获取表名,再代入${tableName}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baomidou.base.MyBaseMapper">
<!-- 清空表-->
<update id="truncateTable">
<bind name="tableName" value="@com.baomidou.mybatisplus.core.metadata.TableInfoHelper@getTableInfo(entityClass).tableName"/>
TRUNCATE TABLE ${tableName}
</update>
</mapper>
2.7 UserTest.class
package com.baomidou;
import com.baomidou.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class UserTest {
@Autowired
UserMapper userMapper;
@Test
void test1() {
userMapper.truncateTable(User.class);
}
}
运行后日志正确,数据库也完成清空

尝试后没有找到其他获取表名的方法,所以如果都采用方案2,那就需要所有的方法都传入实体类,这肯定不太合理,你UserMapper继承MyBaseMapper的时候就传递了泛型,这时候还要我再传一遍,其实等价于传入一个表名了,如果我还要考虑逻辑删除,不可能我再传入一个参数
3. 方案3
提一下注解的方式,也无法实现,连通过实体类获取表名都做不到了。
六、MyBatis-Plus SQL注入器
以上方法核心的问题是在xml中无法获得表的相关信息,那你肯定能想到,我们在Java中编写不就好了,如你所想,MyBatis-Plus官方提供了SQL注入器,这通过 sqlInjector 全局配置实现。通过实现 ISqlInjector 接口或继承 AbstractSqlInjector 抽象类,你可以注入自定义的通用方法到 MyBatis 容器中。
MyBatis-Plus官网 SQL注入器
包含以下四个步骤:
-
- 自定义 BaseMapper
-
- 自定义 SQL方法
-
- 自定义 SQL注入器,注册自定义方法
-
- 配置 SQL注入器
1. 自定义 BaseMapper
创建自定义BaseMapper如MyBaseMapper,继承BaseMapper,声明自定义SQL方法
package com.baomidou.base;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface MyBaseMapper<T> extends BaseMapper<T> {
// 截断表
void truncateTable();
// 批量插入(返回成功插入行数)
int insertBatch(@Param("list") List<T> entityList);
}
2. 自定义 SQL方法
每个自定义SQL方法需要创建一个实现类,继承AbstractMethod,在里面重写SQL的实现代码
2.1 截断表 TruncateTableMethod
package com.baomidou.method;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
public class TruncateTableMethod extends AbstractMethod {
public TruncateTableMethod() {
// 设置方法名为 MyBaseMapper 中对应自定义SQL方法名
super("truncateTable");
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 自定义SQL语句
String sql = "TRUNCATE TABLE " + tableInfo.getTableName();
// 把SQL语句用 <script> 标签包起来创建一个sql源对象,平常 mybatis 用来读取xml中的SQL语句
SqlSource sqlSource = super.createSqlSource(configuration, "<script>" + sql + "</script>", modelClass);
// this.methodName 如果在构造方法中设置了方法名,这里可以省略;如果没有,这里必须传入 MyBaseMapper 中对应自定义SQL方法名
return this.addDeleteMappedStatement(mapperClass, this.methodName, sqlSource);
}
}
2.2 批量插入 MyInsertBatchMethod
package com.baomidou.method;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.util.stream.Collectors;
public class MyInsertBatchMethod extends AbstractMethod {
public MyInsertBatchMethod() {
// 设置方法名为 MyBaseMapper 中对应自定义SQL方法名
super("myInsertBatch");
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 定义SQL语句 (主键+普通字段) insert into table(....) values(....),(....)
String sql = "INSERT INTO " + tableInfo.getTableName() + "(" + tableInfo.getKeyColumn() + "," +
tableInfo.getFieldList().stream().map(TableFieldInfo::getColumn).collect(Collectors.joining(",")) + ") VALUES ";
String value = "(" + "#{" + ENTITY + DOT + tableInfo.getKeyProperty() + "}" + ","
+ tableInfo.getFieldList().stream().map(tableFieldInfo -> "#{" + ENTITY + DOT + tableFieldInfo.getProperty() + "}")
.collect(Collectors.joining(",")) + ")";
String valuesScript = SqlScriptUtils.convertForeach(value, "list", null, ENTITY, COMMA);
SqlSource sqlSource = super.createSqlSource(configuration, "<script>" + sql + valuesScript + "</script>", modelClass);
KeyGenerator keyGenerator = tableInfo.getIdType() == IdType.AUTO ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
// 第三个参数必须和 baseMapper 的自定义方法名一致
return this.addInsertMappedStatement(mapperClass, modelClass, this.methodName, sqlSource, keyGenerator,tableInfo.getKeyProperty(), tableInfo.getKeyColumn());
}
}
3. 自定义 SQL注入器,注册自定义方法
接下来,你需要创建一个自定义 SQL注入器类,通过继承DefaultSqlInjector,并重写getMethodList方法来注册你的自定义方法。
package com.baomidou.injector;
import com.baomidou.method.MyInsertBatchMethod;
import com.baomidou.method.TruncateTableMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.session.Configuration;
import java.util.List;
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(configuration, mapperClass, tableInfo);
methodList.add(new TruncateTableMethod());
methodList.add(new MyInsertBatchMethod());
return methodList;
}
}
4. 配置 SQL注入器
到目前为止,自定义SQL方法还都只是在静态类中,还不能被mybatis识别到,以下三种配置方式选一种即可
4.1 MyBatisPlusConfig
通过@Bean注解将自定义的 Injector 注入 Spring 容器中,以替换默认的 Injector
package com.baomidou.config;
import com.baomidou.injector.MySqlInjector;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
// 如果自定义了sqlSessionFactory,需要在sqlSessionFactory中进行设置。
@Bean
public ISqlInjector sqlInjector() {
return new MySqlInjector();
}
}
4.2 application.yml
mybatis-plus:
global-config:
sql-injector: com.example.MyLogicSqlInjector
4.3 application.properties
mybatis-plus.global-config.sql-injector=com.example.MyLogicSqlInjector
5. 注意事项
- 在定义自定义方法时,确保方法名与注入的SQL语句中的ID一致。
- 在使用自定义的批量插入和自动填充功能时,确保在Mapper方法的参数上使用@Param注解,并且命名符合MyBatis-Plus的默认支持(list, collection, array)。
- 自定义的SQL语句应该根据你的业务需求来编写,确保它能够正确地执行你想要的操作。
6. 测试
package com.baomidou;
import com.baomidou.entity.User;
import com.baomidou.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
public class UserTest {
@Autowired
UserMapper userMapper;
@Test
void test1() {
userMapper.truncateTable();
List<User> userList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setName("姓名"+i);
userList.add(user);
}
userMapper.myInsertBatch(userList);
}
}
自定义方法截断表执行成功,自定义方法批量插入也是一次性传递所有参数

7. InsertBatchSomeColumn
在查找过程中,还发现了 Mybatis Plus 内部提供的默认批量插入,只不过这个方法作者只在 MySQL 数据测试过,所以没有将它作为通用方法供外部调用
package com.baomidou.mybatisplus.extension.injector.methods;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.util.List;
import java.util.function.Predicate;
/**
* 批量新增数据,自选字段 insert
* <p> 不同的数据库支持度不一样!!! 只在 mysql 下测试过!!! 只在 mysql 下测试过!!! 只在 mysql 下测试过!!! </p>
* <p> 除了主键是 <strong> 数据库自增的未测试 </strong> 外理论上都可以使用!!! </p>
* <p> 如果你使用自增有报错或主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!! </p>
* <p>
* 自己的通用 mapper 如下使用:
* <pre>
* int insertBatchSomeColumn(List<T> entityList);
* </pre>
* </p>
*
* <li> 注意: 这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值, insert 后数据库字段是为 null 而不是默认值 </li>
*
* <p>
* 常用的 {@link Predicate}:
* </p>
*
* <li> 例1: t -> !t.isLogicDelete() , 表示不要逻辑删除字段 </li>
* <li> 例2: t -> !t.getProperty().equals("version") , 表示不要字段名为 version 的字段 </li>
* <li> 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略为 UPDATE 的字段 </li>
*
* @author miemie
* @since 2018-11-29
*/
public class InsertBatchSomeColumn extends AbstractMethod {
/**
* 字段筛选条件
*/
@Setter
@Accessors(chain = true)
private Predicate<TableFieldInfo> predicate;
/**
* 默认方法名
*/
public InsertBatchSomeColumn() {
super("insertBatchSomeColumn");
}
/**
* 默认方法名
*
* @param predicate 字段筛选条件
*/
public InsertBatchSomeColumn(Predicate<TableFieldInfo> predicate) {
super("insertBatchSomeColumn");
this.predicate = predicate;
}
/**
* @param name 方法名
* @param predicate 字段筛选条件
* @since 3.5.0
*/
public InsertBatchSomeColumn(String name, Predicate<TableFieldInfo> predicate) {
super(name);
this.predicate = predicate;
}
@SuppressWarnings("Duplicates")
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, null, false) +
this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;
String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, ENTITY_DOT, false) +
this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);
insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;
String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (tableInfo.havePK()) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
// 去除转义符
keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn());
} else {
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
}
}
想使用它的话需要先在MyBaseMapper中声明

再在我们刚刚自定义的SQL注入器里面注册这个方法就行

8. 注入原理分析
偷个懒,搜索一篇放上来:
MyBatis-Plus BaseMapper 实现原理(SQL 注入器的使用及原理解析)
七、性能对比
总结一下前面提到过的所用批量插入方法:
- for循环插入
- BaseMapper.insert
- IService.saveBatch
- 自定义批量插入
- InsertBatchSomeColumn
同时JDBC的rewriteBatchedStatements参数有true/false两种选择
最后进行测试对比一下时间,测试之前关闭mybatis-plus日志,避免控制台打印造成的影响
1. user表
为了提高难度并且更符合实际生产,给user表添加四个字段,创建时间默认CURRENT_TIMESTAMP,创建人id默认1,更新时间勾选根据当前时间戳更新,更新人id默认为null

2. 测试类
@SpringBootTest
public class UserTest {
@Autowired
UserMapper userMapper;
@Autowired
IUserService userService;
void truncate() {
userMapper.truncateTable(); // 每次测试前情况表
}
List<User> userList() {
List<User> userList = new ArrayList<>();
for (int i = 0; i < 300000; i++) {
User user = new User();
user.setName("姓名"+i);
userList.add(user);
}
return userList;
}
}
3. for循环插入
@Test
void test1() {
truncate();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (User user : userList()) {
userMapper.insert(user);
}
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}
4. BaseMapper.insert
@Test
void test2() {
truncate();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
userMapper.insert(userList());
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}
5. IService.saveBatch
@Test
void test3() {
truncate();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
userService.saveBatch(userList());
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}
6. 自定义批量插入
@Test
void test4() {
truncate();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
List<User> userList = userList();
int batchSize = 1000; // 每批插入的大小
int totalBatches = (int) Math.ceil((double) userList.size() / batchSize); // 计算批次数量
for (int i = 0; i < totalBatches; i++) { // 按批次插入数据
int start = i * batchSize; // 计算当前批次的起始和结束索引
int end = Math.min(start + batchSize, userList.size());
List<User> batch = userList.subList(start, end); // 获取当前批次的数据
userMapper.myInsertBatch(batch);
}
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}
7. InsertBatchSomeColumn
@Test
void test5() {
truncate();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
List<User> userList = userList();
int batchSize = 1000; // 每批插入的大小
int totalBatches = (int) Math.ceil((double) userList.size() / batchSize); // 计算批次数量
for (int i = 0; i < totalBatches; i++) { // 按批次插入数据
int start = i * batchSize; // 计算当前批次的起始和结束索引
int end = Math.min(start + batchSize, userList.size());
List<User> batch = userList.subList(start, end); // 获取当前批次的数据
userMapper.insertBatchSomeColumn(batch);
}
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}
8. 耗时统计(单位:ms)
| 插入方法 | 正常url | 添加rewtire参数 |
|---|---|---|
| for循环 | 755266 | - |
| BaseMapper.insert | 749483 | 3828 |
| IService.saveBatch | 15913 | 3178 |
| 自定义批量插入 | 4277 | 4231 |
| InsertBatchSomeColumn | 3639 | 3586 |
以上数据仅测试一次,但数量级比较明显,具有对比性,
可以看出BaseMapper普通的insert根本就是和for循环一样,只不过for循环需要每次发送一遍语句,BaseMapper.insert是每次发送一个参数;
IService.saveBatch还是起到了一定的批处理效果,应该是一次发送了所有参数,只不过mysql仍然是一条条执行,这样也大量节省了通信时间;
自定义方法则没有太大区别,自己写的比官方的稍微慢不到一秒,有需要还是用官方的好
加上rewriteBatchedStatements参数后,for循环太长了就没测试,也影响不到它,其余四种就基本一样了
八、总结
如果只是批量插入,那么使用BaseMapper.insert和url设置rewriteBatchedStatements=true即可
不建议用IService,参考:【抽丝剥茧知识讲解】引入mybtis-plus后,mapper实现方式
如果需要自定义公共SQL方法,那么按照第六部分的教程进行注入即可
应用到微服务项目,单独创建mybatisplus模块,文件目录如下

想要在其他模块引入使用,还需要创建resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,在里面添加配置类相对路径,具体请看:
【沉浸式解决问题】微服务子模块引入公共模块的依赖后无法bean未注入
参考文献
MyBatis-Plus官网 SQL注入器
【MyBatis】mybatis-plus 批量插入 性能优化
关于jdbc批量操作(addBatch, executeBatch)的测试【转载】
MySQL JDBC 实战: PreparedStatement rewriteBatchedStatements 实现原理
Mybatis Plus 批量 Insert_新增数据(图文讲解)
MyBatis-Plus BaseMapper 实现原理(SQL 注入器的使用及原理解析)
后记
这是写博客以来最详细最细致的一篇了,花了好几个晚上,断断续续终于是完成了。写这么细不单单是分享自定义BaseMapper,更是想通过研究一个简单的问题,分享研究方法,解决能力,逻辑思维,希望能给大家带来一点收获和启发,谢谢!
喜欢的点个赞和收藏吧><!祝你永无bug!
/*
_ooOoo_
o8888888o
88" . "88
(| -_- |)
O\ = /O
____/`---'\____
.' \\| |// `.
/ \\||| : |||// \
/ _||||| -:- |||||- \
| | \\\ - /// | |
| \_| ''\---/'' | |
\ .-\__ `-` ___/-. /
___`. .' /--.--\ `. . __
."" '< `.___\_<|>_/___.' >'"".
| | : `- \`.;`\ _ /`;.`/ - ` : | |
\ \ `-. \_ __\ /__ _/ .-` / /
======`-.____`-.___\_____/___.-`____.-'======
`=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
佛祖保佑 永无BUG
*/


被折叠的 条评论
为什么被折叠?



