原创辟谣,实测MyBatisPlus批量新增/更新方法确实有效,且可单独使用无需跟随IService
Posted DCTANT
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原创辟谣,实测MyBatisPlus批量新增/更新方法确实有效,且可单独使用无需跟随IService相关的知识,希望对你有一定的参考价值。
前言
之前看网上说MyBatisPlus(后面简称MP)的批量新增、更新方法只是简单是for循环insert/update,性能毫无差别,我就觉得奇怪了,这么严重的问题作者就没有发现吗,难不成还得自己去写批量新增方法?
这里批判以下两篇博客,简直误人子弟
https://www.cnblogs.com/thinkYi/p/13723035.html
https://blog.csdn.net/leisure_life/article/details/98976565
还有就是这个批量新增方法仅仅只能在IService中implement一下才能使用,如果在别的Service调用非本类的Entity不就用不了了。比如说主表是一个Service实现IService,用的主表的Entity,那我如果要在主表的Service中去批量插入关联表的Entity列表,那我还怎么用,难不成去Autowired关联表的Service,那逻辑岂不是乱套了,代码的耦合性也太强了,这明显有问题啊。
成品方法
废话不多说,直接上代码:
public static <T extends BaseEntity, R extends BaseMapper<T>> void saveBatch(Class<R> mapperClass, List<T> entityList)
saveBatch(mapperClass, entityList, 1000);
public static <T extends BaseEntity, R extends BaseMapper<T>> void saveBatch(Class<R> mapperClass, List<T> entityList, int batchSize)
if (entityList.size() == 0)
return;
T t = entityList.get(0);
Class<T> entityClass = (Class<T>) t.getClass();
SqlHelper.saveOrUpdateBatch(entityClass, mapperClass, log, entityList, batchSize, (sqlSession, entity) ->
// INFO: DCTANT: 2021/12/27 insert判断,返回true则是走insert代码,返回false则会走后面的update代码
if (entity == null)
return false;
Long id = entity.getId();
if (id == null)
// INFO: DCTANT: 2021/12/27 insert前加一些自己必要的业务逻辑,如setCreateTime、setDel、setVersion等等
insertNecessaryField(entity);
return true;
else
// INFO: DCTANT: 2021/12/27 去执行update的代码
return false;
, (sqlSession, entity) ->
// INFO: DCTANT: 2021/12/27 判断为update,然后执行必要操作
if (entity == null)
return;
// INFO: DCTANT: 2021/12/27 update前加一些自己的业务逻辑,如setUpdateTime等等
updateNecessaryField(entity);
sqlSession.update(SqlHelper.getSqlStatement(mapperClass, SqlMethod.UPDATE_BY_ID), entityList);
);
其中SqlHelper是MP中自己的代码,我直接拿出来复用罢了,里面的逻辑可比别的博客自己写的业务逻辑强太多了。BaseEntity是我所有Entity的一个基类,包含了id、createTime、updateTime、version等基础字段,BaseMapper是MP自己的BaseMapper。
我相当于在原来MP作者的SqlHelper.saveOrUpdateBatch()方法基础上再次封装了一层罢了,尽量贴近原生。
源码分析
然后分析一下MP自己的SqlHelper.saveOrUpdateBatch()方法,说实话这代码可读性真的很糟糕,我研究了好久才搞明白,如果不是会Kotlin,这东西真难搞懂
public static <E> boolean saveOrUpdateBatch(Class<?> entityClass, Class<?> mapper, Log log, Collection<E> list, int batchSize, BiPredicate<SqlSession, E> predicate, BiConsumer<SqlSession, E> consumer)
String sqlStatement = getSqlStatement(mapper, SqlMethod.INSERT_ONE);
return executeBatch(entityClass, log, list, batchSize, (sqlSession, entity) ->
if (predicate.test(sqlSession, entity))
sqlSession.insert(sqlStatement, entity);
else
consumer.accept(sqlSession, entity);
);
第一个入参entity,自己的class,这个不多说,保存的就是这玩意的类型
第二个参数mapper,这个是MP的BaseMapper的继承接口,注意!!这个入参千万不能是@Autowired出来的XXXMapper,因为这个类是Spring动态代理生成的,根本不是原来的类!!我栽在这个坑上花了一个多小时才发现这个问题!!必须要填XXXMapper.class,而不能用@Autowired出来的XXXMapper去getClass(),这样会直接报错!原因是:
public static String getSqlStatement(Class<?> mapper, SqlMethod sqlMethod)
return mapper.getName() + "." + sqlMethod.getMethod();
getSqlStatement中的mapper.getName拿到的是Proxy代理类,类似于com.sun.proxy.$Proxy128.insert,而不是真正的类名!!
第三个参数log,日志罢了,没什么特殊的LogFactory.getLog(XXX.class);即可获取到
第四个参数list,真正要保存的就是这个列表
第五个参数batchSize,session中满多少个实例flush一次
第六个参数predicate,用lambda的写法就是(sqlSession,entity)->Boolean,给你两个参数,sqlSession和entity,在这里随你想做什么就做什么,最后返回一个boolean类型就行了。返回的boolean用于后面判断是新增还是编辑
第七个参数consume,用lambda的写法就是(sqlSession,entity)->Void,给你两个参数sqlSession和entity,想干嘛就干嘛,最后都不用你返回值。这里面主要用来做编辑(update)操作,由于源码中没有执行sqlSession.update()方法,因此这里的编辑方法得自己写。
重头戏其实在executeBatch里,这里就是sqlSession中insert/update一定数量的之后去flush结果
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer)
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession ->
int size = list.size();
int i = 1;
for (E element : list)
consumer.accept(sqlSession, element);
if ((i % batchSize == 0) || i == size)
sqlSession.flushStatements();
i++;
);
这里的consumer.accpet中执行的就是我们的一堆sqlSession.insert()和sqlSession.update()方法,到了if ((i % batchSize == 0) || i == size)成立后sqlSession.flushStatements(),代码完全没有问题,根本不是上述两篇博客中写的无脑for循环insert。
性能测试
最后是性能测试:
我自己的测试表包含8个字段
代码分别使用MP的SqlHelper.saveOrUpdateBatch和for循环insert方法,接口采用OkHttp的方式请求,分别测试1000次、2000次、5000次、10000次、20000次,记录所耗费的时间:
public RespVo testBatchSpeed(ExampleEo exampleEo)
long startTime = System.currentTimeMillis();
Integer number = exampleEo.getNumber();
Boolean insert = exampleEo.getInsert();
ArrayList<ExampleEntity> exampleEntities = new ArrayList<>();
for (int i = 0; i < number; i++)
ExampleEntity exampleEntity = new ExampleEntity();
if (!insert)
exampleEntity.setId((long) (i + 1));
exampleEntity.setName("speed test " + i + " " + System.currentTimeMillis());
exampleEntity.setNumber(i);
exampleEntities.add(exampleEntity);
saveBatch(exampleEntities);
log.info(ElapseTimeOutputUtil.printString("batch save,总量:"+number+" 消耗时间:", startTime, System.currentTimeMillis()));
return success();
public RespVo testForSpeed(ExampleEo exampleEo)
long startTime = System.currentTimeMillis();
Integer number = exampleEo.getNumber();
Boolean insert = exampleEo.getInsert();
ArrayList<ExampleEntity> exampleEntities = new ArrayList<>();
for (int i = 0; i < number; i++)
ExampleEntity exampleEntity = new ExampleEntity();
if (!insert)
exampleEntity.setId((long) (i + 1));
exampleEntity.setName("speed test " + i + " " + System.currentTimeMillis());
exampleEntity.setNumber(i);
exampleEntities.add(exampleEntity);
for (ExampleEntity exampleEntity : exampleEntities)
save(exampleEntity);
log.info(ElapseTimeOutputUtil.printString("for save,总量:"+number+" 消耗时间:", startTime, System.currentTimeMillis()));
return success();
结果如图所示:
使用MP自带的SqlHelper.saveOrUpdateBatch()方法的效率的for循环单个操作的两倍性能还多,且批量操作的数量越大,效果越明显。
最后希望这篇博客能给大家带来收获,如果有错误的地方请大家指出。
以上是关于原创辟谣,实测MyBatisPlus批量新增/更新方法确实有效,且可单独使用无需跟随IService的主要内容,如果未能解决你的问题,请参考以下文章