前言
之前看网上说MyBatisPlus(后面简称MP)的批量新增、更新方法只是简单是for循环insert/update,性能毫无差别,我就觉得奇怪了,这么严重的问题作者就没有发现吗,难不成还得自己去写批量新增方法?
这里批判以下两篇博客,简直误人子弟
还有就是这个批量新增方法仅仅只能在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循环单个操作的两倍性能还多,且批量操作的数量越大,效果越明显。
最后希望这篇博客能给大家带来收获,如果有错误的地方请大家指出。
=========================================================
2022年8月22日更新:
勘误:SqlHelper.saveOrUpdateBatch的重写update的方法在新版本MybatisPlus中会出问题,正确的写法为:
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, userId);
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, userId);
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
// INFO: DCTANT: 2022/8/22 参数需要为Constants.ENTITY,也就是里面的et,否则会报et这个参数无法找到
param.put(Constants.ENTITY, entity);
String sqlStatement = SqlHelper.getSqlStatement(mapperClass, SqlMethod.UPDATE_BY_ID);
sqlSession.update(sqlStatement, param);
});