Mybatis plus Sql注入器实现批量插入的研究

Mybatis plus Sql注入器实现批量插入的研究

记录mybatsi plus用sql注入器实现批量插入的过程,并解析

背景

假如说我有这么个场景,我只能用mybatis plus 的baseMapper进行开发,然后我又有个批量插入的需求,然后我去调用baseMapper的通用方法,发现没有批量插入的功能。这种时候怎么办呢?
我们自定义mapper的通用功能,或者是扩展baseMapper的通用功能。(实现其他功能扩展也是类似的)

原理

其实不管前面的实现流程有多复杂,最终的原理就是要生成类似mapper.xml里面的标签语句,然后注入到mapper里面。例如批量插入到user表的功能,最后生成的语句就是:

<script>
	INSERT INTO 
  	user (id,name,age,email) 
  VALUES 
  <foreach collection="list" item="et" separator=",">
  	(#{et.id},#{et.name},#{et.age},#{et.email})
	</foreach>
</script>

这段代码是不是很熟悉,这就是我们自己手动写批量插入的时候要写在xml的sql语句嘛。所以我们后面代码前面的繁琐操作都可以套模板即可,主要是围绕如何拿到表、字段信息,如何用这些信息拼接出我们需要的sql模板出来。

步骤

  1. 写一个自定义的通用方法类,使其继承 AbstractMethod 类
  2. 实现其 injectMappedStatement 方法,根据方法可获得的参数,拼接出通用的mapper.xml的脚本,如上示例,并使其与对应mapper关联
  3. 自定义sql注入器,使其继承 DefaultSqlInjector 类,并把自定义的通用方法类添加到sql注入器中
  4. 在配置类中把 sql注入器对象配置到Spring容器中
  5. 自定义一个通用mapper接口,使其继承mybatis plus提供的Mapper接口

实现

以下只为尽量快展示具体要怎么做,不需要太了解,大概看一眼有什么东西后可以直接到 解释 去看对应代码的讲解。

自定义通用方法类SaveBatch

public class SaveBatch extends AbstractMethod {

    private Predicate<TableFieldInfo> predicate;

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

        KeyGenerator keyGenerator = new NoKeyGenerator();
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) + this.filterTableFieldInfo(fieldList, this.predicate, TableFieldInfo::getInsertSqlColumn, "");
        String columnScript = "(" + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + ")";
        String insertSqlProperty = tableInfo.getKeyInsertSqlProperty("et.", false) + this.filterTableFieldInfo(fieldList, this.predicate, (i) -> {
            return i.getInsertSqlProperty("et.");
        }, "");
        insertSqlProperty = "(" + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + ")";
        String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", (String)null, "et", ",");
        String keyProperty = null;
        String keyColumn = null;
        if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
            if (tableInfo.getIdType() == IdType.AUTO) {
                keyGenerator = new Jdbc3KeyGenerator();
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            } else if (null != tableInfo.getKeySequence()) {
                keyGenerator = TableInfoHelper.genKeyGenerator(this.getMethod(sqlMethod), tableInfo, this.builderAssistant);
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            }
        }

        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
        SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, this.getMethod(sqlMethod), sqlSource, (KeyGenerator)keyGenerator, keyProperty, keyColumn);
    }

    public String getMethod(SqlMethod sqlMethod) {
        return "saveBatch";
    }

    public SaveBatch() {
    }

    public SaveBatch(final Predicate<TableFieldInfo> predicate) {
        this.predicate = predicate;
    }

    public SaveBatch setPredicate(final Predicate<TableFieldInfo> predicate) {
        this.predicate = predicate;
        return this;
    }
}

自定义sql注入器

/**
 * sql注入器
 */
public class MySqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //增加自定义方法
        methodList.add(new SaveBatch(i -> i.getFieldFill() != FieldFill.UPDATE));
        return methodList;
    }
}

把Sql注入器注入Spring容器

@Configuration
@MapperScan("org.example.mapper")
public class MybatisPlusConfig {

    /**
     * 把sql注入器注入Spring容器
     * @return
     */
    @Bean
    public MySqlInjector sqlInjector(){
        return new MySqlInjector();
    }
}

自定义通用mapper

public interface CommonMapper<T> extends Mapper<T> {
    //定义我们自己实现的通用方法
    int saveBatch(@Param("list") List<T> batchList);
}

测试

实体类
@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
UserMapper
public interface UserMapper extends CommonMapper<User> { //继承自定义通用mapper
}
测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MybatisPlusApplicationTests {

	@Resource
	private UserMapper userMapper;

	@Test
	public void testSelectList() {
		ArrayList<User> users = new ArrayList<>();
		for (int i = 0; i <10 ; i++){User user = new User();
			user.setAge(20);
			user.setName("Jone");
			user.setEmail("test1@baomidou.com");
			user.setName(user.getName() + i);
			users.add(user);
		}
		userMapper.saveBatch(users);
	}
}
效果

在这里插入图片描述
注意这里并不是最终实现哦,直接拿来用是会出问题的。这里还能继续优化,如果要真实实际拿来用,请直接跳转到最终实现节点哈!

解释

SaveBatch

预热

关键点解析,以user表为测试表为基准

  • SqlMethod
package com.baomidou.mybatisplus.core.enums;

public enum SqlMethod {
    INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>\nINSERT INTO %s %s VALUES %s\n</script>"),
    UPSERT_ONE("upsert", "Phoenix插入一条数据(选择字段插入)", "<script>\nUPSERT INTO %s %s VALUES %s\n</script>"),
    DELETE_BY_ID("deleteById", "根据ID 删除一条数据", "<script>\nDELETE FROM %s WHERE %s=#{%s}\n</script>"),
    DELETE_BY_MAP("deleteByMap", "根据columnMap 条件删除记录", "<script>\nDELETE FROM %s %s\n</script>"),
    DELETE("delete", "根据 entity 条件删除记录", "<script>\nDELETE FROM %s %s %s\n</script>"),
    DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量删除数据", "<script>\nDELETE FROM %s WHERE %s IN (%s)\n</script>"),
    LOGIC_DELETE_BY_ID("deleteById", "根据ID 逻辑删除一条数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
	...
    
    private final String method;
    private final String desc;
    private final String sql;

    private SqlMethod(String method, String desc, String sql) {
        this.method = method;
        this.desc = desc;
        this.sql = sql;
    }

    public String getMethod() {
        return this.method;
    }

    public String getDesc() {
        return this.desc;
    }

    public String getSql() {
        return this.sql;
    }
}

从上面可以看到,这个是mybatis plus定义的一个sql方法枚举类。里面主要包含三个部分:方法名,方法描述,方法脚本模板。
我们关注两个点:

  1. 方法名
    这些方法名是不是很熟悉?就是我们平时用自己的mapper继承BaseMapper后可以直接调用的一些方法。结合我们自己定义的SaveBatch类,我们不妨可以做这么一个猜想:
    会不会这里的一个名字就是对应了一个mybatis plus自己实现了的通用方法的类?
    我们实现自己的扩展方法的话,是不是也应该自己给扩展方法定义一个名字?
    在这里插入图片描述
    查看源码好像真的是这么回事
  2. 方法脚本模板
    例如我们这里写段代码
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;

这一段,对应的枚举对象为 INSERT_ONE。对应的方法脚本模板为

<script>\nINSERT INTO %s %s VALUES %s\n</script>

想想看我们获得 INSERT_ONE 对象是为了什么?
为了这段脚本。
"%s"表示替换参数。你看这段脚本的三个参数符的位置,刚好对应 表名、字段名列表、值参数列表 的位置。想想是不是对应我们前面说的要拼接出通用sql的模板。
假如接下来我们想办法获取到表名、字段名列表、值参数列表的字符串,执行这么一段代码:

        /**
         * sqlMethod.getSql() : <script>\nINSERT INTO %s %s VALUES %s\n</script>
         * tableInfo.getTableName() : user
         * columnScript : (name,age,email)
         * valuesScript : <foreach collection="list" item="et" separator=",">
         *                      (#{et.name},#{et.age},#{et.email})
         *                </foreach>
         */
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);

翻译过来就是

String sql = String.format("<script>\nINSERT INTO %s %s VALUES %s\n</script>", 
                           "user", 
                           "(name,age,email)", 
                           "<foreach collection=\"list\" item=\"et\" separator=\",\">
								(#{et.name},#{et.age},#{et.email})
							</foreach>");

结果就是上面我们的示例脚本

<script>
	INSERT INTO 
  		user (id,name,age,email) 
  	VALUES 
  	<foreach collection="list" item="et" separator=",">
  	(#{et.id},#{et.name},#{et.age},#{et.email})
	</foreach>
</script>

这样模板sql不就实现了?当然我们自己写的 SaveBatch类里面也是这么写的。你往下翻翻,看看实现方法的下面也是这么写的。

当然这个脚本你也可以不用sqlMethod对象的,可以自己写个类似的字符串

实现
public class SaveBatch extends AbstractMethod {

    private Predicate<TableFieldInfo> predicate;

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

        KeyGenerator keyGenerator = new NoKeyGenerator();
        //获取sqlMethod对象,主要是获取sqlMethod的sql脚本模板字符串 <script>\nINSERT INTO %s %s VALUES %s\n</script>
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        //在表信息对象中获取字段信息集合 name,age,email 可以看出没算进id,因为方便把主键拿出来单独处理
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        //通过一些流程处理,获取字段列表脚本字符串 insertSqlColumn : name,age,email 
        //这里前半段主要是判断主键策略,如果是自增长就不拼接id进去了
        String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) + this.filterTableFieldInfo(fieldList, this.predicate, TableFieldInfo::getInsertSqlColumn, "");
        //columnScript : (name,age,email)
        String columnScript = "(" + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + ")";
        //insertSqlProperty : #{et.name},#{et.age},#{et.email}
        //这里et很重要的,其实是对应<foreach>标签中item属性的值,也就是遍历集合时每个元素对象的名字
        //这里也是做了点主键策略判断处理
        String insertSqlProperty = tableInfo.getKeyInsertSqlProperty("et.", false) + this.filterTableFieldInfo(fieldList, this.predicate, (i) -> {
            return i.getInsertSqlProperty("et.");
        }, "");
        //(#{et.name},#{et.age},#{et.email})
        insertSqlProperty = "(" + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + ")";
        // convertForeach 函数为帮我们把 insertSqlProperty 与 <foreach>相关字符串进行拼接
        // 注意"list"与我们后面写的通用mapper方法的参数的@Param("list")要对应
        String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", (String)null, "et", ",");
        String keyProperty = null;
        String keyColumn = null;
        //主键生成策略判断
        if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
            if (tableInfo.getIdType() == IdType.AUTO) {
                keyGenerator = new Jdbc3KeyGenerator();
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            } else if (null != tableInfo.getKeySequence()) {
                keyGenerator = TableInfoHelper.genKeyGenerator(this.getMethod(sqlMethod), tableInfo, this.builderAssistant);
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            }
        }
    	// 根据前面获取的参数生成通用sql脚本
        /**
    	sql:
    	<script>
        	INSERT INTO 
          		user (id,name,age,email) 
          	VALUES 
          	<foreach collection="list" item="et" separator=",">
          		(#{et.id},#{et.name},#{et.age},#{et.email})
        	</foreach>
        </script>
        */
        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
        //绑定并添加这个sql脚本到对应mapper里面
        //类似于在userMapper.xml文件中写了一个saveBatch方法,脚本就是上面的sql
        //modelClass: class org.example.entity.User
        //mapperClass: interface org.example.mapper.UserMapper
        SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, this.getMethod(sqlMethod), sqlSource, (KeyGenerator)keyGenerator, keyProperty, keyColumn);
    }

    //返回一个字符串,为我们定义的方法名
    public String getMethod(SqlMethod sqlMethod) {
        return "saveBatch";
    }

    public SaveBatch() {
    }

    public SaveBatch(final Predicate<TableFieldInfo> predicate) {
        this.predicate = predicate;
    }

    public SaveBatch setPredicate(final Predicate<TableFieldInfo> predicate) {
        this.predicate = predicate;
        return this;
    }
}

MySqlInjector

我们自定义的这个sql注入器,让其继承mp的 AbstractSqlInjector ,我们可以看看这个类的内容

public abstract class AbstractSqlInjector implements ISqlInjector {
    ...

    public AbstractSqlInjector() {
    }

    public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
                ...
                //调用抽象方法获取通用方法类,抽象方法由子类实现
                List<AbstractMethod> methodList = this.getMethodList(mapperClass);
                if (CollectionUtils.isNotEmpty(methodList)) {
                    TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                    methodList.forEach((m) -> {
                        m.inject(builderAssistant, mapperClass, modelClass, tableInfo);
                    });
                ...
    }
	//我们需要实现的抽象方法
    public abstract List<AbstractMethod> getMethodList(Class<?> mapperClass);

    protected Class<?> extractModelClass(Class<?> mapperClass) {
    	...
    }
}

去掉不需要关心的代码,可以看到,这里面其实主要用了设计模式中的模板方法模式,就是其他关键方法都实现了,在其他方法中调用抽象方法,然后把抽象方法留给子类去实现。根据子类实现方法的不同,根据返回结果 AbstractSqlInjector 其他被继承的方法的执行结果也会不同。
getMethodList 方法返回的是 List 集合,而我们的SaveBatch类是继承 AbstractMethod 的,所以我们实现 getMethodList 方法时,把我们的 SaveBatch类添加到 List 集合并返回出去即可。
如下:

/**
 * sql注入器
 */
public class MySqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //增加自定义方法
        methodList.add(new SaveBatch(i -> i.getFieldFill() != FieldFill.UPDATE));
        return methodList;
    }
}

到这里我又要提出问题了:
我们平时继承BaseMapper后就能调用各种通用方法,那它是不是也应该有一个对应的sql注入器?我们自定义sql注入器并注入Spring容器以后,是覆盖原来的注入器,还是往后累加?
我们做如下实验

针对自定义sql注入器是否覆盖默认注入器的实验
  1. 重新让UserMapper继承BaseMapper
public interface UserMapper extends BaseMapper<User> {}
  1. 调用通用方法
	@Test
	public void testSelectList() {
		userMapper.selectList(null);
	}
  1. 结果
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): org.example.mapper.UserMapper.selectList

这里报错的意思大概就是找不到这个selectList方法。所以这里说明我们自定义的sql注入器会覆盖默认的sql注入器。

这里其实是想引出并验证一下Spring Boot的默认配置问题。就是说我们平时用redis,mysql等等各种组件,添加依赖后就能用了,要改啥配置为啥加一下就又能指定为自己的了?那就是因为有默认的配置,我们自己再配置后就会覆盖原来的配置呀。当然我现在才疏学浅对于Spring Boot源码Mybatis源码之类的还没到学习的时候,但是大概了解过的一些知识,还是觉得亲手验证一下会比较踏实。

好的,这里又有新的问题了。
我们的sql注入器覆盖了原来的sql注入器,那我们还想用原来的通用方法怎么办?
到这里就又要提一下了。既然我们自己能定义通用的方法SaveBatch,对应一个SaveBatch类,那我们平时调用的通用方法是不是也应该一一对应一个通用方法的实现类?还记得这张图吧:
在这里插入图片描述
这下就明了了,如果我们还要用自己平时用的通用方法的话,可以这么去改造我们的sql注入器:

public class MySqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = new ArrayList<>();
        //增加自定义方法
        methodList.add(new SaveBatch(i -> i.getFieldFill() != FieldFill.UPDATE));
        //增加mybatis plus默认提供的SelectList类,相当于增加selectList通用方法
        methodList.add(new SelectList());
        return methodList;
    }
}

再次测试

	@Test
	public void testSelectList() {
		userMapper.selectList(null);
	}

发现方法正常执行,日志正常打印。说明把方法添加进去的操作是合理的。

==>  Preparing: SELECT id,name,age,email FROM user 
==> Parameters: 
<==    Columns: id, name, age, email
<==        Row: 1, Jone, 18, test1@baomidou.com
<==        Row: 2, Jack, 20, test2@baomidou.com
<==        Row: 3, Tom, 28, test3@baomidou.com
<==        Row: 4, Sandy, 21, test4@baomidou.com
<==        Row: 5, Billie, 24, test5@baomidou.com
<==        Row: 6, Jone0, 20, test1@baomidou.com
...

问题又来了:如果我还要添加其他方法,我们要一个个手动 new 然后添加进去吗?显然是不合理的。
这时候我们就要想起来刚才我们猜测的,mp应该有自己的默认的sql注入器吧,能不能拿来用一下呢?
当然可以。

DefaultSqlInjector

DefaultSqlInjector 就是这个默认的sql注入器。看看源码

public class DefaultSqlInjector extends AbstractSqlInjector {
    public DefaultSqlInjector() {
    }

    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new DeleteById(), new DeleteBatchByIds(), new Update(), new UpdateById(), new SelectById(), new SelectBatchByIds(), new SelectByMap(), new SelectOne(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList());
    }
}
  1. 这个sql注入器也是继承了 AbstractSqlInjector 并实现其 getMethodList
  2. 这个 getMethodList 方法里面返回了一个 List 集合,里面就包含了各种我们需要的mp提供的通用方法实现类

所以我们可以这么做:

  1. 让我们自定义的sql注入器继承DefaultSqlInjector 类
  2. 重写 getMethodList 的过程中,调用父类(DefaultSqlInjector )已实现的 getMethodList 方法,获取集合结果,把我们自定义的通用方法SaveBatch类追加进去,再返回出去
/**
 * sql注入器
 * 继承 DefaultSqlInjector 间接也是继承了 AbstractSqlInjector
 */
public class MySqlInjector extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        //获取父类已实现的集合
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //追加自定义方法
        methodList.add(new SaveBatch(i -> i.getFieldFill() != FieldFill.UPDATE));
        return methodList;
    }
}

这样我们就可以在自定义方法的基础上,又包含了原有的所有通用方法了。

把sql注入器注入Spring容器

这个就没啥说的了

CommonMapper

这个是我们自定义的通用mapper。再次看看源码,我们来看看有什么问题

public interface CommonMapper<T> extends Mapper<T> {
    int saveBatch(@Param("list") List<T> batchList);
}

看看Mapper

public interface Mapper<T> {
}

这里我们会发现,CommonMapper 里只有一个我们自定义的方法,Mapper里面又什么都没有。如果我们让UserMapper继承 CommonMapper 的话,那我们也只能调用这个通用方法了。在这里我们也能联系起来了吧,要用通用方法,首先要在Sql注入器里把通用方法注入进去,然后就是要在通用mapper里面定义,以让我们的实体类对应Mapper(如UserMapper)继承通用Mapper后可以调用方法,就像调用BaseMapper的方法一样。这样我们也不难猜测,BaseMapper里面应该也是有很多默认定义好的,对应mp的默认通用方法类的方法。
看看BaseMapper

public interface BaseMapper<T> extends Mapper<T> {
    int insert(T entity);

    int deleteById(Serializable id);

    int deleteByMap(@Param("cm") Map<String, Object> columnMap);

    int delete(@Param("ew") Wrapper<T> wrapper);

    int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);

    int updateById(@Param("et") T entity);

    int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

    T selectById(Serializable id);

    List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);

    List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);

    T selectOne(@Param("ew") Wrapper<T> queryWrapper);

    Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);

    List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);

    List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);

    List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);

    <E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper);

    <E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper);
}

所以我们又要调用mp的默认通用方法,又要调用自定义的通用方法,应该这么做:

  1. 让 CommonMapper 继承 BaseMapper
  2. 在 CommonMapper 追加我们自定义的通用方法
  3. 让实体类对应Mapper(UserMapper)继承 CommonMapper
public interface CommonMapper<T> extends BaseMapper<T> {
    int saveBatch(@Param("list") List<T> batchList);
}
public interface UserMapper extends CommonMapper<User>{
}

至此我们的所有优化也做完了,所以最终实现如下:

最终实现

//通用方法实现类
public class SaveBatch extends AbstractMethod {

    private Predicate<TableFieldInfo> predicate;

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

        KeyGenerator keyGenerator = new NoKeyGenerator();
        //获取sqlMethod对象,主要是获取sqlMethod的sql脚本模板字符串 <script>\nINSERT INTO %s %s VALUES %s\n</script>
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        //在表信息对象中获取字段信息集合 name,age,email 可以看出没算进id,因为方便把主键拿出来单独处理
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        //通过一些流程处理,获取字段列表脚本字符串 insertSqlColumn : name,age,email 
        //这里前半段主要是判断主键策略,如果是自增长就不拼接id进去了
        String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) + this.filterTableFieldInfo(fieldList, this.predicate, TableFieldInfo::getInsertSqlColumn, "");
        //columnScript : (name,age,email)
        String columnScript = "(" + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + ")";
        //insertSqlProperty : #{et.name},#{et.age},#{et.email}
        //这里et很重要的,其实是对应<foreach>标签中item属性的值,也就是遍历集合时每个元素对象的名字
        //这里也是做了点主键策略判断处理
        String insertSqlProperty = tableInfo.getKeyInsertSqlProperty("et.", false) + this.filterTableFieldInfo(fieldList, this.predicate, (i) -> {
            return i.getInsertSqlProperty("et.");
        }, "");
        //(#{et.name},#{et.age},#{et.email})
        insertSqlProperty = "(" + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + ")";
        // convertForeach 函数为帮我们把 insertSqlProperty 与 <foreach>相关字符串进行拼接
        // 注意"list"与我们后面写的通用mapper方法的参数的@Param("list")要对应
        String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", (String)null, "et", ",");
        String keyProperty = null;
        String keyColumn = null;
        //主键生成策略判断
        if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
            if (tableInfo.getIdType() == IdType.AUTO) {
                keyGenerator = new Jdbc3KeyGenerator();
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            } else if (null != tableInfo.getKeySequence()) {
                keyGenerator = TableInfoHelper.genKeyGenerator(this.getMethod(sqlMethod), tableInfo, this.builderAssistant);
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            }
        }
    	// 根据前面获取的参数生成通用sql脚本
        /**
    	sql:
    	<script>
        	INSERT INTO 
          		user (id,name,age,email) 
          	VALUES 
          	<foreach collection="list" item="et" separator=",">
          		(#{et.id},#{et.name},#{et.age},#{et.email})
        	</foreach>
        </script>
        */
        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
        //绑定并添加这个sql脚本到对应mapper里面
        //类似于在userMapper.xml文件中写了一个saveBatch方法,脚本就是上面的sql
        //modelClass: class org.example.entity.User
        //mapperClass: interface org.example.mapper.UserMapper
        SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, this.getMethod(sqlMethod), sqlSource, (KeyGenerator)keyGenerator, keyProperty, keyColumn);
    }

    //返回一个字符串,为我们定义的方法名
    public String getMethod(SqlMethod sqlMethod) {
        return "saveBatch";
    }

    public SaveBatch() {
    }

    public SaveBatch(final Predicate<TableFieldInfo> predicate) {
        this.predicate = predicate;
    }

    public SaveBatch setPredicate(final Predicate<TableFieldInfo> predicate) {
        this.predicate = predicate;
        return this;
    }
}
/**
 * 自定义sql注入器
 */
public class MySqlInjector extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        //获取 DefaultSqlInjector 中已注入的通用方法列表
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //增加自定义方法
        methodList.add(new SaveBatch(i -> i.getFieldFill() != FieldFill.UPDATE));
        return methodList;
    }
}
//mp配置类
@Configuration
@MapperScan("org.example.mapper")
public class MybatisPlusConfig {

    /**
     * 把sql注入器注入Spring容器
     * @return
     */
    @Bean
    public MySqlInjector sqlInjector(){
        return new MySqlInjector();
    }
}
//自定义通用mapper
public interface CommonMapper<T> extends BaseMapper<T> {
    int saveBatch(@Param("list") List<T> batchList);
}
//表对应实体类的mapper
public interface UserMapper extends CommonMapper<User>{
}
//实体类
@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

测试

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MybatisPlusApplicationTests {

	@Resource
	private UserMapper userMapper;

	@Test
	public void testSelectList() {
		List<User> users = userMapper.selectList(null);
		users.stream().forEach(user -> user.setId(null));
		userMapper.saveBatch(users);
	}
}

在这里插入图片描述

补充

文中提到的SaveBatch实现类中的代码,其实不是我写的。mp自己提供了一个类 insertBatchSomeColumn 实现了这段代码,但是没有提供saveBatch方法,我只是拿过来用并测试了一下而已。最终目的只是为了能解析自定义注入器的一个实现流程以及其中的一些细节。

这是我第一次尝试写文章。为了好好写一篇文章,要调试,记录,会花费很多时间。下班时间也少,只能分了两天写。希望至少质量是保证的,如果表达得不太好,还请见谅。

语雀地址:Mybatis plus Sql注入器实现批量插入的研究

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值