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>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
完成上述步骤,就可以运行项目进行测试看看,数据提升是不是几个数量级。
拓展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( 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; # mybatis
所有评论(0)