众所周知,mybatisplus提供的BaseMapper里只有单条插入的方法,没有批量插入的方法,
而在Service层的批量插入并不是真的批量插入,实际上是遍历insert,但也不是一次insert就一次IO,而是到一定数量才会去IO一次,性能不是很差,但也不够好。
怎么才能实现真正的批量插入呢?
这里是mybatisplus官方的演示仓库,可以先去了解一下。
一、注册自定义通用方法流程
- 把自定义方法写到BaseMapper,因为没法改BaseMapper,所以继承一下它
language-java1 2 3 4 5
| public interface MyBaseMapper<T> extends BaseMapper<T> {
int batchInsert(@Param("list") List<T> entityList); }
|
MyBaseMapper扩展了原有的BaseMapper,所以你之后的Mapper层都继承自MyBaseMapper而不是BaseMapper即可。
- 把通用方法注册到mybatisplus
language-java1 2 3 4 5 6 7 8 9 10 11
| @Component public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> defaultMethodList = super.getMethodList(mapperClass, tableInfo); defaultMethodList.add(new BatchInsert("batchInsert")); return defaultMethodList; } }
|
关键的一句在于defaultMethodList.add(new BatchInsert("batchInsert"));
,意为注册一个新的方法叫batchInsert
,具体实现在BatchInsert
类。
- 实现BatchInsert类
language-java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| public class BatchInsert extends AbstractMethod { public BatchInsert(String name) { super(name); }
@Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; SqlMethod sqlMethod = SqlMethod.INSERT_ONE; String columnScript = getAllInsertSqlColumn(tableInfo.getFieldList()); String valuesScript = SqlScriptUtils.convertForeach(LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET, LIST, null, "item", COMMA); String keyProperty = null; String keyColumn = null; // 表包含主键处理逻辑,如果不包含主键当普通字段处理 if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) { if (tableInfo.getIdType() == IdType.AUTO) { /* 自增主键 */ keyGenerator = Jdbc3KeyGenerator.INSTANCE; keyProperty = tableInfo.getKeyProperty(); // 去除转义符 keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn()); } else if (null != tableInfo.getKeySequence()) { keyGenerator = TableInfoHelper.genKeyGenerator(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); }
/** * 获取 insert 时所有列名组成的sql片段 * @param fieldList 表字段信息列表 * @return sql 脚本片段 */ private String getAllInsertSqlColumn(List<TableFieldInfo> fieldList) { return LEFT_BRACKET + fieldList.stream() .map(TableFieldInfo::getColumn).filter(Objects::nonNull).collect(joining(COMMA + NEWLINE)) + RIGHT_BRACKET; }
/** * 获取 insert 时所有属性值组成的sql片段 * @param prefix 前缀 * @param fieldList 表字段信息列表 * @return sql 脚本片段 */ private String getAllInsertSqlProperty(final String prefix, List<TableFieldInfo> fieldList) { final String newPrefix = prefix == null ? EMPTY : prefix; return fieldList.stream() .map(i -> i.getInsertSqlProperty(newPrefix).replace(",", "")) .filter(Objects::nonNull) .collect(joining(COMMA + NEWLINE)); } }
|
二、BatchInsert具体实现逻辑解析
以如下简单的表user
举例,
列名 |
描述 |
类型 |
id |
主键,自增 |
bigint |
user_name |
用户名 |
varchar(64) |
user_age |
用户年龄 |
int |
对于的entity
大抵如下
language-java1 2 3 4 5 6 7 8 9 10 11
| @Data @EqualsAndHashCode(callSuper = true) @Accessors(chain = true) @TableName("user") public class User{ @TableId(value = "id", type = IdType.AUTO) private Long id; private String userName; private int userAge; }
|
那么对于batchInsert
,我们希望传入List<User>
并希望得到类似如下的mybtaisplus xml sql
语句
language-xml1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <insert id="batchInsert" parameterType="java.util.List"> insert into user( id, user_name, user_age )values <foreach collection="list" item="item" separator=","> ( #{item.id}, #{item.userName}, #{item.userAge} ) </foreach> </insert>
|
但是我们并不自己写这个xml
,不然这需要对每一个数据表都要写一个,就像不那么硬的硬代码一样,我们希望有段逻辑,只需要传入entity
,就能自己解析其中列名和对应的属性名,生成这段xml
实现批量插入的功能。
假设你的UserMapper
已经继承自MyBaseMapper
,如果调用UserMapper.bacthInsert(List<User> entityList)
,那么会进入这个函数
language-java1 2
| @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo)
|
其中mapperClass
是映射器类,modelClass
是模型类,我们并不需要了解,最主要的是tableInfo
,这是表信息,它包含了关于数据库表的各种信息,如表名、列名、主键等。这个参数提供了详细的表信息,这对于生成针对特定表的SQL语句是必要的。
然后执行如下
language-java1 2 3 4 5 6 7
| //如果你的表名没有主键,那么你需要指定keyGenerator 为NoKeyGenerator, //因为重写injectMappedStatement最后需要返回return this.addInsertMappedStatement //其中就需要KeyGenerator KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; //SqlMethod.INSERT_ONE就是"INSERT INTO %s %s VALUES %s" //我们依据表的信息生成列名sql片段和属性名sql片段后填入%s就可以得到近似最后的xml sql SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
|
然后执行如下
language-java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| //tableInfo.getFieldList()会得到一个包含数据表列信息(不包含主键)的TableFieldInfo类组成的List String columnScript = getAllInsertSqlColumn(tableInfo.getFieldList()); //这行代码就是在调用这个函数 private String getAllInsertSqlColumn(List<TableFieldInfo> fieldList) { return LEFT_BRACKET + fieldList.stream() //从TableFieldInfo中只拿取列名 .map(TableFieldInfo::getColumn) //过滤null .filter(Objects::nonNull) //在元素间以逗号和分行分割 .collect(joining(COMMA + NEWLINE)) + RIGHT_BRACKET; } //对于User表,这个函数返回以下String /* (user_name, user_age) */
|
然后执行如下
language-java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| //首先调用了getAllInsertSqlProperty String valuesScript = SqlScriptUtils.convertForeach( // 这也是个内置函数,可以直接去看看 LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET, LIST, null, "item", COMMA ); //LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET //得到 /* (#{userName}, #{userAge}) */ //经过convertForeach函数后,得到如下字符串 /* <foreach collection="list" item="item" separator=","> (#{userName}, #{userAge}) </foreach> */
//getAllInsertSqlProperty函数如下 private String getAllInsertSqlProperty(final String prefix, List<TableFieldInfo> fieldList) { //这里newPrefix 就是"item." final String newPrefix = prefix == null ? EMPTY : prefix; return fieldList.stream() //i.getInsertSqlProperty("item.")是内置函数,假设i现在遍历到了user_name列 //那么得到的就是"#{userName}," //然后,被删了 //所以本来每个元素从TableFieldInfo变成了形如"#{userName}"的字符串 .map(i -> i.getInsertSqlProperty(newPrefix).replace(",", "")) .filter(Objects::nonNull) //在元素间插入逗号和分行 .collect(joining(COMMA + NEWLINE)); } //对于User表,这个函数返回以下String /* #{userName}, #{userAge} */
|
然后执行如下
language-java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| //定义主键属性名 String keyProperty = null; //定义主键列名 String keyColumn = null; // 表包含主键处理逻辑,如果不包含主键当普通字段处理 if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) { if (tableInfo.getIdType() == IdType.AUTO) { /* 自增主键 */ keyGenerator = Jdbc3KeyGenerator.INSTANCE; keyProperty = tableInfo.getKeyProperty(); // 去除转义符 keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn()); } else if (null != tableInfo.getKeySequence()) { keyGenerator = TableInfoHelper.genKeyGenerator(methodName, tableInfo, builderAssistant); keyProperty = tableInfo.getKeyProperty(); keyColumn = tableInfo.getKeyColumn(); } } //这段代码没什么好说的,就是根据不同情况,得到三个变量 //keyGenerator keyProperty keyColumn
|
然后执行如下
language-java1 2 3 4 5 6 7 8 9
| String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript); //就是把表名user,列名片段,属性名片段,填入%s中,得到如下 /* INSERT INTO user (user_name, user_age) VALUES <foreach collection="list" item="item" separator=","> (#{userName}, #{userAge}) </foreach> */
|
然后执行如下
language-java1 2 3 4
| //这两句没有什么可说的,是重写injectMappedStatement函数的必要的操作 //自定义的内容就在于sql和主键 SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass); return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
|