前言

之前看网上说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个字段

mybatis plus BaseMapper批量新增 mybatis批量更新或新增_SpringBoot

 代码分别使用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();
    }

结果如图所示:

mybatis plus BaseMapper批量新增 mybatis批量更新或新增_MyBatisPlus_02

 使用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);
        });