Mybatis-Plus 关于savebatch,saveorupdatebatch遇到的坑及解决办法&拓展mybatisPlus实现类方法,批量插入时,唯一索引冲突后更新

一.背景

​ 最近mybatis-plus框架的更新,让我们基础开发中如虎添翼。其中基本的增删改查,代码生成器想必大家用着那叫一个爽。本人在使用中,也遇到一些坑。比如savebatch,saveorupdatebatch,看着这不是批量新增,批量新增或更新嘛,看着api进行开发,感觉也太好用啦。开发完一测试,速度跟蜗牛一样,针对大数据量真是无法忍受。在控制台上发现,怎么名义上是批量插入,还是一条一条的进行插入,难怪速度龟速。

二.解决办法

查阅网上资料,大体有两种解决方案:

(1).使用mybatis的xml,自己进行sql语句编写。该方法一个缺点是如果表的字段较多,有个几十个字段,写批量新增,批量新增修改的sql语句真是个噩梦。

INSERT INTO t 
    (id, age) 
VALUES 
    (3, 28),
    (4, 29) 
ON DUPLICATE KEY UPDATE
    id = VALUES(id),
    age = VALUES(age);

(2)mybatis-plus 新添加了一个sql注入器,通过sql注入器可以实现批量新增,批量新增修改功能。一次注入,随时使用,使用极其方便。缺点就是项目启动时候,会进行sql注入器注册,稍微影响启动速度。

三.sql注入器实现批量更新,批量插入或更新功能

(1)自定义mapper接口,继承BaseMapper,定义实现的方法。

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
 
import java.util.List;
 
/**
 * 根Mapper,给表Mapper继承用的,可以自定义通用方法
 * {@link BaseMapper}
 * {@link com.baomidou.mybatisplus.extension.service.IService}
 * {@link com.baomidou.mybatisplus.extension.service.impl.ServiceImpl}
 */
public interface RootMapper<T> extends BaseMapper<T> {
    
    
 
    /**
     * 自定义批量插入
     * 如果要自动填充,@Param(xx) xx参数名必须是 list/collection/array 3个的其中之一
     */
    int insertBatch(@Param("list") List<T> list);
 
    /**
     * 自定义批量新增或更新
     * 如果要自动填充,@Param(xx) xx参数名必须是 list/collection/array 3个的其中之一
     */
    int mysqlInsertOrUpdateBath(@Param("list") List<T> list);
 
}

(2)批量插入、批量新增或更新具体方法实现

批量插入具体方法实现如下:

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
 
/**
 * 批量插入方法实现
 */
@Slf4j
public class InsertBatchMethod extends AbstractMethod {
    
    
    /**
     * insert into user(id, name, age) values (1, "a", 17), (2, "b", 18);
     <script>
     insert into user(id, name, age) values
     <foreach collection="list" item="item" index="index" open="(" separator="),(" close=")">
     #{item.id}, #{item.name}, #{item.age}
     </foreach>
     </script>
     */
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    
    
        final String sql = "<script>insert into %s %s values %s</script>";
        final String fieldSql = prepareFieldSql(tableInfo);
        final String valueSql = prepareValuesSql(tableInfo);
        final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);
        log.debug("sqlResult----->{}", sqlResult);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
        // 第三个参数必须和RootMapper的自定义方法名一致
        return this.addInsertMappedStatement(mapperClass, modelClass, "insertBatch", sqlSource, new NoKeyGenerator(), null, null);
    }
 
    private String prepareFieldSql(TableInfo tableInfo) {
    
    
        StringBuilder fieldSql = new StringBuilder();
        fieldSql.append(tableInfo.getKeyColumn()).append(",");
        tableInfo.getFieldList().forEach(x -> {
    
    
            fieldSql.append(x.getColumn()).append(",");
        });
        fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
        fieldSql.insert(0, "(");
        fieldSql.append(")");
        return fieldSql.toString();
    }
 
    private String prepareValuesSql(TableInfo tableInfo) {
    
    
        final StringBuilder valueSql = new StringBuilder();
        valueSql.append("<foreach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\"),(\" close=\")\">");
        valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
        tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
        valueSql.delete(valueSql.length() - 1, valueSql.length());
        valueSql.append("</foreach>");
        return valueSql.toString();
    }
}

批量插入或更新具体方法如下:

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
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;
 
public class MysqlInsertOrUpdateBath extends AbstractMethod {
    
    
 
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    
    
        final String sql = "<script>insert into %s %s values %s ON DUPLICATE KEY UPDATE %s</script>";
        final String tableName = tableInfo.getTableName();
        final String filedSql = prepareFieldSql(tableInfo);
        final String modelValuesSql = prepareModelValuesSql(tableInfo);
        final String duplicateKeySql =prepareDuplicateKeySql(tableInfo);
        final String sqlResult = String.format(sql, tableName, filedSql, modelValuesSql,duplicateKeySql);
        //System.out.println("savaorupdatesqlsql="+sqlResult);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, "mysqlInsertOrUpdateBath", sqlSource, new NoKeyGenerator(), null, null);
    }
 
    /**
     * 准备ON DUPLICATE KEY UPDATE sql
     * @param tableInfo
     * @return
     */
    private String prepareDuplicateKeySql(TableInfo tableInfo) {
    
    
        final StringBuilder duplicateKeySql = new StringBuilder();
        if(!StringUtils.isEmpty(tableInfo.getKeyColumn())) {
    
    
            duplicateKeySql.append(tableInfo.getKeyColumn()).append("=values(").append(tableInfo.getKeyColumn()).append("),");
        }
 
        tableInfo.getFieldList().forEach(x -> {
    
    
            duplicateKeySql.append(x.getColumn())
                    .append("=values(")
                    .append(x.getColumn())
                    .append("),");
        });
        duplicateKeySql.delete(duplicateKeySql.length() - 1, duplicateKeySql.length());
        return duplicateKeySql.toString();
    }
 
    /**
     * 准备属性名
     * @param tableInfo
     * @return
     */
    private String prepareFieldSql(TableInfo tableInfo) {
    
    
        StringBuilder fieldSql = new StringBuilder();
        fieldSql.append(tableInfo.getKeyColumn()).append(",");
        tableInfo.getFieldList().forEach(x -> {
    
    
            fieldSql.append(x.getColumn()).append(",");
        });
        fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
        fieldSql.insert(0, "(");
        fieldSql.append(")");
        return fieldSql.toString();
    }
 
    private String prepareModelValuesSql(TableInfo tableInfo){
    
    
        final StringBuilder valueSql = new StringBuilder();
        valueSql.append("<foreach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\"),(\" close=\")\">");
        if(!StringUtils.isEmpty(tableInfo.getKeyProperty())) {
    
    
            valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
        }
        tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
        valueSql.delete(valueSql.length() - 1, valueSql.length());
        valueSql.append("</foreach>");
        return valueSql.toString();
    }
}

(3)sql注入器实现

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
 
import java.util.List;
 
/**
 * 自定义方法SQL注入器
 */
public class CustomizedSqlInjector extends DefaultSqlInjector {
    
    
    /**
     * 如果只需增加方法,保留mybatis plus自带方法,
     * 可以先获取super.getMethodList(),再添加add
     */
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
    
    
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        methodList.add(new InsertBatchMethod());
        methodList.add(new UpdateBatchMethod());
        methodList.add(new MysqlInsertOrUpdateBath());
        return methodList;
    }
}

(4)在自己想使用的mapper上继承自定义的mapper.

import com.sy.adp.common.mybatisPlusExtend.RootMapper;
import com.sy.adp.flowfull.entity.InfMpmPds;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
 
@Component
public interface InfMpmPdsMapper extends RootMapper<InfMpmPds> {
    
    
 
    IPage<InfMpmPds> selectPageList(Page page, @Param("infMpmPds") InfMpmPds infMpmPds);
 
}

(5)在controller或serviceImpi中引入mapper,使用自定义的方法。

>>>>>>>>>>>>>>>>>>>>引入mapper>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

img

>>>>>>>>>>>>>>>>>>>>>>>>>方法使用>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

img

完成上述步骤,就可以运行项目进行测试看看,数据提升是不是几个数量级。




拓展mybatisPlus实现类方法,批量插入时,唯一索引冲突后更新

唯一索引注解

package com.zk.fahai.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FhId {
    
    }

拓展服务类,使用时extend即可

package com.zk.fahai.util;

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
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.Assert;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zk.fahai.common.annotation.FhId;
import com.zk.fx.common.util.JsonUtils;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Transactional;

  /** 批量插入,冲突更新 @param entityList 数据 */
  @Transactional(rollbackFor = Exception.class)
  public void saveOrUpdateBatchByFhId(Collection<T> entityList) throws IOException {
    
    
    TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
    Assert.notNull(
        tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");

    List<String> ukFieldNameList =
        tableInfo.getFieldList().stream()
            .filter(c -> c.getField().isAnnotationPresent(FhId.class))
            .map(tableFieldInfo -> tableFieldInfo.getField().getName())
            .collect(Collectors.toList());
    if (ukFieldNameList.size() < 1) {
    
    
      logger.error("未找到唯一索引,class:{}", tableInfo.getEntityType());
      return;
    }
    for (List<T> list : this.cutList(entityList, IService.DEFAULT_BATCH_SIZE)) {
    
    
      try {
    
    
        super.saveBatch(list);
      } catch (DuplicateKeyException duplicateKeyException) {
    
    
        for (T entity : list) {
    
    
          try {
    
    
            super.save(entity);
          } catch (DuplicateKeyException e2) {
    
    
            logger.info("数据更新触发唯一索引,对象:{}", JsonUtils.object2JsonString(entity));
            UpdateWrapper<T> updateWrapper = new UpdateWrapper<>();
            ukFieldNameList.forEach(
                uk -> updateWrapper.eq(uk, ReflectionKit.getFieldValue(entity, uk)));
            super.update(entity, updateWrapper);
          }
        }
      }
    }
  }

  /** 切分集合 */
  public <C> List<List<C>> cutList(Collection<C> list, int maxNum) {
    
    
    int step = (list.size() + maxNum - 1) / maxNum;
    return Stream.iterate(0, n -> n + 1)
        .limit(step)
        .parallel()
        .map(
            a ->
                list.stream()
                    .skip(a * maxNum)
                    .limit(maxNum)
                    .parallel()
                    .collect(Collectors.toList()))
        .collect(Collectors.toList());
  }
}

不使用saveBatch版,拓展服务类,使用时extend即可

发现oracle使用savebatch时有问题

package com.zk.fahai.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 法海数据唯一索引
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FhUniqueIndex {
    
    }
package com.zk.fahai.service.fh;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
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.Assert;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zk.fahai.common.annotation.FhUniqueIndex;
import com.zk.fx.common.util.JsonUtils;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Transactional;

/**
 * 拓展 mybatis-plus方法,数据落库使用
 *
 */
public class DataStorageExpandService<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> {
    
    

  private static final Logger logger = LoggerFactory.getLogger(DataStorageExpandService.class);

  /** 插入,冲突更新 @param entityList 数据 */
  @Transactional(rollbackFor = Exception.class)
  public void saveOrUpdateByFhUk(List<T> entityList) throws IOException {
    
    
    List<String> fhUkFieldNameList = this.getFhUkFieldNameList();
    for (T entity : entityList) {
    
    
      this.saveOrUpdateByFhUniqueIndex(fhUkFieldNameList, entity);
    }
  }

  /** 插入,冲突更新 @param entity 数据 */
  @Transactional(rollbackFor = Exception.class)
  public void saveOrUpdateByFhUk(T entity) throws IOException {
    
    
    this.saveOrUpdateByFhUniqueIndex(this.getFhUkFieldNameList(), entity);
  }

  private void saveOrUpdateByFhUniqueIndex(List<String> fhUkFieldNameList, T entity)
      throws IOException {
    
    
    try {
    
    
      super.save(entity);
    } catch (DuplicateKeyException e2) {
    
    
      QueryWrapper<T> queryWrapper = new QueryWrapper<>();
      fhUkFieldNameList.forEach(
          ukFieldName -> {
    
    
            Object ukFieldValue = ReflectionKit.getFieldValue(entity, ukFieldName);
            queryWrapper.eq(ukFieldName, ukFieldValue);
          });
      this.fillId(entity, queryWrapper);
      logger.info("数据保存触发唯一索引,进行更新,数据 ->{}", JsonUtils.object2JsonString(entity));
      super.updateById(entity);
    }
  }

  /** 获取主键回填 */
  private void fillId(T entity, QueryWrapper<T> queryWrapper) {
    
    
    TableInfo tableInfo = TableInfoHelper.getTableInfo(super.entityClass);
    Assert.notNull(
        tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
    String keyProperty = tableInfo.getKeyProperty();
    Assert.notEmpty(
        keyProperty, "error: can not execute. because can not find column for id from entity!");
    Long idVal = (Long) ReflectionKit.getFieldValue(super.getOne(queryWrapper), keyProperty);
    try {
    
    
      Field idField = entity.getClass().getDeclaredField(keyProperty);
      idField.setAccessible(true);
      idField.set(entity, idVal);
    } catch (NoSuchFieldException | IllegalAccessException e) {
    
    
      logger.error("获取主键回填异常", e);
    }
  }

  private List<String> getFhUkFieldNameList() {
    
    
    TableInfo tableInfo = TableInfoHelper.getTableInfo(super.entityClass);
    Assert.notNull(
        tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
    List<TableFieldInfo> tableFieldInfoList =
        tableInfo.getFieldList().stream()
            .filter(c -> c.getField().isAnnotationPresent(FhUniqueIndex.class))
            .collect(Collectors.toList());
    Assert.notEmpty(
        tableFieldInfoList,
        "error: can not execute. because can not find @FhUniqueIndex for entity!");
    List<String> fhUkFieldNameList =
        tableFieldInfoList.stream()
            .map(tableFieldInfo -> tableFieldInfo.getField().getName())
            .collect(Collectors.toList());
    Assert.notEmpty(
        fhUkFieldNameList,
        "error: can not execute. because can not find column for @FhUniqueIndex from entity!");
    return fhUkFieldNameList;
  }
}

猜你喜欢

转载自blog.csdn.net/qq_43842093/article/details/132956726