原创辟谣,实测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的主要内容,如果未能解决你的问题,请参考以下文章

原创获取MybatisPlus注入的mapper的真实类型

原创获取MybatisPlus注入的mapper的真实类型

[原创]K8Cscan4.0之Base64/HEX密码批量加密解密插件以及源码

oracle批量新增更新数据

mybatisplus 更新字段为null

MyBatisPlus总结