springboot 无缝贴合mybatis-plus源码,扩展动态批量插入和更新的方法

1 适用

  1. 适用于根据实体类字段的不同,动态的生成插入或更新的mapper,不用每个手写
  2. 只要表有主键,批量插入更新会自动判断是插入还是更新,业务代码无需关注
  3. 当更新动作时,不需要更新的实体字段只要为null即可,不需要考虑实体合并

2 背景

在使用mybatis或者mybatis-plus的时候,遇到这种场景:批量插入和更新。一般做法是,有数据就插入,没数据就更新,中间不免就有一个查询数据的环节,在mybatisplus源码里就是这样干的,相当于保存数据前根据查询结果将数据列表分成了两组,一组是插入组,一组是更新组。这时数据保存的性能就有待考虑了。

3 使用前提和注意事项

使用前提

  • 数据库类型为mysql
  • 数据表有主键

注意事项

在数据类型不为字符串时,就不要使用下面这用法。会出现比如date类型不能和string比较的错误

<if test="item.apiId != null and item.apiId != ''">#{item.apiId},</if>

可以使用以下方式

<if test="item.apiId != null>#{item.apiId},</if>

4 用法1 普通用法

在xml这样搞,属于常规的一套流程

<insert id="insertOrUpdateBatch"  parameterType="java.util.List">  
insert into tb_test
	<trim prefix="( " suffixOverrides="," suffix=") ">
		<if test="list[0].apiId != null and list[0].apiId != ''">api_id,</if>
		<if test="list[0].orgId != null and list[0].orgId != ''">org_id,</if>
		<if test="list[0].orgName != null and list[0].orgName != ''">org_name,</if> 
	</trim>
 values
	<foreach collection="list" item="item" index="index" separator=",">
		<trim prefix="( " suffixOverrides="," suffix=") ">
			<if test="item.apiId != null and item.apiId != ''">#{item.apiId},</if>
			<if test="item.orgId != null and item.orgId != ''">#{item.orgId},</if>
			<if test="item.orgName != null and item.orgName != ''">#{item.orgName},</if> 
		</trim>
	</foreach>
 ON DUPLICATE KEY UPDATE 
	<trim prefix=" " suffixOverrides="," suffix=" ">
		<if test="list[0].apiId != null and list[0].apiId != ''">api_id=values(api_id),</if>
		<if test="list[0].orgId != null and list[0].orgId != ''">org_id=values(org_id),</if>
		<if test="list[0].orgName != null and list[0].orgName != ''">org_name=values(org_name),</if> 
	</trim>
</insert>

5 用法2 mybatis plus扩展mapper用法

此用法是基于mybatis plus源码,无缝贴合扩展,关键代码只需两步

5.1 定制mapper方法,绑定生成的sql片段

定制mapper,通俗讲就是动态生成上面xml的sql内容

import cn.自己的项目.bridge.mapper.extend.MysqlKeywords;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.springframework.util.StringUtils;

import java.util.stream.Collectors;

/**
 * 适用于mysql,按列字段名进行更新操作,有值的列进行插入或更新,没值的列不做处理,保留原样
 * 其中,动态的列时动态变化
 * 样例:
 * INSERT INTO `test` (`date`, `time_str`,  `xny_rq`)
 * VALUES
 * ('xxxx-01-01', '01:00', 111)
 * ON DUPLICATE KEY UPDATE
 * `date` = values(`date`),time_str=VALUES(`time_str`),xny_rq=VALUES(xny_rq);
 */
public class DynamicColumnInsertOrUpdateBath extends AbstractMethod {

   @Override
   public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
      String sqlTemplate = BridgeSqlMethod.DynamicColumnInsertOrUpdateBath.getSql();
      String formatSql =
         String.format(sqlTemplate, tableInfo.getTableName(), dynamicColumns(tableInfo), dynamicValues(tableInfo),
            prepareDuplicateKeySql(tableInfo));
      SqlSource sqlSource = languageDriver.createSqlSource(configuration, formatSql, modelClass);
      return this.addInsertMappedStatement(mapperClass, modelClass,
         BridgeSqlMethod.DynamicColumnInsertOrUpdateBath.getMethod(), sqlSource, new NoKeyGenerator(), null, null);
   }

   private String dynamicValues(TableInfo tableInfo) {
      StringBuilder builder = new StringBuilder();
      final String itemStrKey = "item.%s";
      final String itemStrValue = "#{item.%s},";
      String ifTest = tableInfo.getFieldList()
                               .stream()
                               .map(item -> convertIf(String.format(itemStrKey, item.getProperty()),
                                  String.format(itemStrValue, item.getProperty()), item))
                               .collect(Collectors.joining(NEWLINE));
      String trimSql = trimSql(LEFT_BRACKET, RIGHT_BRACKET, COMMA, ifTest);

      builder.append("<foreach collection=\"list\" item=\"item\" index=\"index\" separator=\",\">")
             .append(NEWLINE).append(trimSql).append(NEWLINE)
             .append("</foreach>").append(NEWLINE);
      return builder.toString();
   }

   /**
    * 获取动态的列
    *
    * @param tableInfo
    * @return
    */
   private String dynamicColumns(TableInfo tableInfo) {
      StringBuilder builder = new StringBuilder();
      if(!StringUtils.isEmpty(tableInfo.getKeyColumn())) {
         builder.append(tableInfo.getKeyColumn()).append(COMMA);
      }
      String collect = tableInfo.getFieldList().stream().map(item -> {
         String column = MysqlKeywords.covertKeyword(item.getColumn());
         return convertIf("list[0]." + item.getProperty(), column + ",", item);
      }).collect(Collectors.joining(NEWLINE));
      String trimSql = trimSql(LEFT_BRACKET, RIGHT_BRACKET, COMMA, collect);
      builder.append(trimSql);
      return builder.toString();
   }


   /**
    * 准备ON DUPLICATE KEY UPDATE sql
    * <if test="%s"> id=values(id) </if>
    *
    * @param tableInfo
    * @return
    */
   private String prepareDuplicateKeySql(TableInfo tableInfo) {
      String ifKey = "list[0].%s";
      String duplicateContext = "%s=values(%s),";
      String ifTest = tableInfo.getFieldList()
                                .stream()
                                .map(tableFieldInfo -> convertIf(String.format(ifKey, tableFieldInfo.getProperty()),
                                   String.format(duplicateContext, tableFieldInfo.getColumn(),
                                      tableFieldInfo.getColumn()), tableFieldInfo))
                                .collect(Collectors.joining(NEWLINE));

      String trimSql = trimSql(EMPTY, EMPTY, COMMA, ifTest);
      return trimSql;
   }


   /**
    * 转换成 if 标签的脚本片段
    *
    * @param testKey   sql 脚本片段
    * @param testValue java字段名,批量处理需要list[0].
    * @return if 脚本片段
    */
   private String convertIf(final String testKey, String testValue, TableFieldInfo tableFieldInfo) {
      String testScript = String.format("%s != null", testKey);
      if(tableFieldInfo.isCharSequence()) {
         testScript = String.format("%s != null and %s != ''", testKey, testKey);
      }

      return String.format("<if test=\"%s\">%s</if>", testScript, testValue);
   }

   /**
    * 截取拼接xml前置和后置内容
    *
    * @param prefix         前置内容
    * @param suffix         后置内容
    * @param suffixOverride 截取的最后字符
    * @param sqlTemplate    中间的内容
    * @return
    */
   private String trimSql(String prefix, String suffix, String suffixOverride, String sqlTemplate) {
      String scriptTemplate = "<trim prefix=\"%s \" suffixOverrides=\"%s\" suffix=\"%s \">\n %s \n</trim>\n";
      return String.format(scriptTemplate, prefix, suffixOverride, suffix, sqlTemplate);
   }
}

5.2 辅助枚举类

public enum BridgeSqlMethod {
   /**
    * 动态列批量插入或更新
    */
   DynamicColumnInsertOrUpdateBath("dynamicColumnInsertOrUpdateBath", "动态列批量插入或更新(选择字段插入)",
      "<script>insert into %s\n %s values\n %s ON DUPLICATE KEY UPDATE \n%s</script>");

   private final String method;

   private final String desc;

   private final String sql;

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

   public String getMethod() {
      return method;
   }

   public String getDesc() {
      return desc;
   }

   public String getSql() {
      return sql;
   }
}

5.3 mapper方法注册,在启动的时候加载

import cn.自己的项目.mapper.extend.method.DynamicColumnInsertOrUpdateBath;
import cn.自己的项目.mapper.extend.method.MysqlInsertOrUpdateBath;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.extension.injector.methods.additional.InsertBatchSomeColumn;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 自定义mapper方法注入器。和mybatis_plus无缝集成
 */
@Component
public class BridgeMapperInjector extends DefaultSqlInjector {
   @Override
   public List<AbstractMethod> getMethodList() {
      List<AbstractMethod> methodList = super.getMethodList();
      methodList.add(new MysqlInsertOrUpdateBath());
      methodList.add(new DynamicColumnInsertOrUpdateBath());
      return methodList;
   }
}

6、关联参考

可参考我上一篇博文

mybatis-plus自定义sql模板,自定义批量更新或插入


版权声明:本文为t18092838767原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。