• 【原创】辟谣,实测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 > void saveBatch(Class mapperClass, List entityList) {
            saveBatch(mapperClass, entityList, 1000);
        }
    
        public static > void saveBatch(Class mapperClass, List entityList, int batchSize) {
            if (entityList.size() == 0) {
                return;
            }
            T t = entityList.get(0);
            Class entityClass = (Class) t.getClass();
            SqlHelper.saveOrUpdateBatch(entityClass, mapperClass, log, entityList, batchSize, (sqlSession, entity) -> {
                // INFO: : 2021/12/27 insert判断,返回true则是走insert代码,返回false则会走后面的update代码
                if (entity == null) {
                    return false;
                }
                Long id = entity.getId();
                if (id == null) {
                    // INFO: : 2021/12/27 insert前加一些自己必要的业务逻辑,如setCreateTime、setDel、setVersion等等
                    insertNecessaryField(entity);
                    return true;
                } else {
                    // INFO: : 2021/12/27 去执行update的代码
                    return false;
                }
            }, (sqlSession, entity) -> {
                // INFO: : 2021/12/27 判断为update,然后执行必要操作 
                if (entity == null) {
                    return;
                }
                // INFO: : 2021/12/27 update前加一些自己的业务逻辑,如setUpdateTime等等
                updateNecessaryField(entity);
                sqlSession.update(SqlHelper.getSqlStatement(mapperClass, SqlMethod.UPDATE_BY_ID), entityList);
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    其中SqlHelper是MP中自己的代码,我直接拿出来复用罢了,里面的逻辑可比别的博客自己写的业务逻辑强太多了。BaseEntity是我所有Entity的一个基类,包含了id、createTime、updateTime、version等基础字段,BaseMapper是MP自己的BaseMapper。

    我相当于在原来MP作者的SqlHelper.saveOrUpdateBatch()方法基础上再次封装了一层罢了,尽量贴近原生。

    源码分析

    然后分析一下MP自己的SqlHelper.saveOrUpdateBatch()方法,说实话这代码可读性真的很糟糕,我研究了好久才搞明白,如果不是会Kotlin,这东西真难搞懂

        public static  boolean saveOrUpdateBatch(Class entityClass, Class mapper, Log log, Collection list, int batchSize, BiPredicate predicate, BiConsumer 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);
                }
    
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    第一个入参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();
        }
    
    • 1
    • 2
    • 3

    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  boolean executeBatch(Class entityClass, Log log, Collection list, int batchSize, BiConsumer 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++;
                }
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这里的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 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 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();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    结果如图所示:

    使用MP自带的SqlHelper.saveOrUpdateBatch()方法的效率的for循环单个操作的两倍性能还多,且批量操作的数量越大,效果越明显。

    最后希望这篇博客能给大家带来收获,如果有错误的地方请大家指出。

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    05-RDD五大特性
    力扣370周赛 -- 第三题(树形DP)
    【项目】API接口的加签和验签
    AIGC驱动产品开发创新,改变你所知的一切!
    SpringMVC框架之Controller方法的返回值
    WebSocket vs SSE: 实时数据推送到前端的选择与实现(详细)
    Oracle-单个PDB迁移升级到19c
    制造业数据标准化的优势分析
    vue2与vue3 v-model的区别
    Django model 联合约束和联合索引
  • 原文地址:https://blog.csdn.net/m0_67393157/article/details/126080758