MybatisPlus批量插入这样操作提升性能
使用的mybatisplus的批量插入方法:saveBatch(),之前就看到过网上都在说在jdbc的url路径上加上rewriteBatchedStatementstrue参数mysql底层才能开启真正的批量插入模式。
保证5。1。13以上版本的驱动,才能实现高性能的批量插入。MySQLJDBC驱动在默认情况下会无视executeBatch()语句,把我们期望批量执行的一组sql语句拆散,一条一条地发给MySQL数据库,批量插入实际上是单条插入,直接造成较低的性能。只有把rewriteBatchedStatements参数置为true,驱动才会帮你批量执行SQL。另外这个选项对INSERTUPDATEDELETE都有效。
目前我的数据表目前是没有建立索引的,即使是在1000来w的数据量下进行1500条的批量插入也不可能消耗20来秒吧,于是矛盾转移到saveBatch方法,使用版本:
查看源码:publicbooleansaveBatch(CollectionTentityList,intbatchSize){StringsqlStatementthis。getSqlStatement(SqlMethod。INSERTONE);returnthis。executeBatch(entityList,batchSize,(sqlSession,entity){sqlSession。insert(sqlStatement,entity);});}protectedEbooleanexecuteBatch(CollectionElist,intbatchSize,BiConsumerSqlSession,Econsumer){returnSqlHelper。executeBatch(this。entityClass,this。log,list,batchSize,consumer);}publicstaticEbooleanexecuteBatch(Classlt;?entityClass,Loglog,CollectionElist,intbatchSize,BiConsumerSqlSession,Econsumer){Assert。isFalse(batchSize1,batchSizemustnotbelessthanone,newObject〔0〕);return!CollectionUtils。isEmpty(list)executeBatch(entityClass,log,(sqlSession){intsizelist。size();inti1;for(Iteratorvar6list。iterator();var6。hasNext();i){Eelementvar6。next();consumer。accept(sqlSession,element);if(ibatchSize0isize){sqlSession。flushStatements();}}});}
最终来到了executeBatch()方法,可以看到这很明显是在一条一条循环插入,通过sqlSession。flushStatements()将一个个单条插入的insert语句分批次进行提交,而且是同一个sqlSession,这相比遍历集合循环insert来说有一定的性能提升,但是这并不是sql层面真正的批量插入。
通过查阅相关文档后,发现mybatisPlus提供了sql注入器,我们可以自定义方法来满足业务的实际开发需求。
sql注入器官网
https:baomidou。compages42ea4a
sql注入器官方示例
https:gitee。combaomidoumybatisplussamplestreemastermybatisplussampledeluxe
在mybtisPlus的核心包下提供的默认可注入方法有这些:
在扩展包下,mybatisPlus还为我们提供了可扩展的可注入方法:
AlwaysUpdateSomeColumnById:根据Id更新每一个字段,全量更新不忽略null字段,解决mybatisplus中updateById默认会自动忽略实体中null值字段不去更新的问题;InsertBatchSomeColumn:真实批量插入,通过单SQL的insert语句实现批量插入;Upsert:更新or插入,根据唯一约束判断是执行更新还是删除,相当于提供insertonduplicatekeyupdate支持。
可以发现mybatisPlus已经提供好了InsertBatchSomeColumn的方法,我们只需要把这个方法添加进我们的sql注入器即可。publicMappedStatementinjectMappedStatement(Classlt;?mapperClass,Classlt;?modelClass,TableInfotableInfo){KeyGeneratorkeyGeneratorNoKeyGenerator。INSTANCE;SqlMethodsqlMethodSqlMethod。INSERTONE;ListTableFieldInfofieldListtableInfo。getFieldList();StringinsertSqlColumntableInfo。getKeyInsertSqlColumn(true,false)this。filterTableFieldInfo(fieldList,this。predicate,TableFieldInfo::getInsertSqlColumn,);拼接批量插入语句StringcolumnScript(insertSqlColumn。substring(0,insertSqlColumn。length()1));StringinsertSqlPropertytableInfo。getKeyInsertSqlProperty(true,et。,false)this。filterTableFieldInfo(fieldList,this。predicate,(i){returni。getInsertSqlProperty(et。);},);insertSqlProperty(insertSqlProperty。substring(0,insertSqlProperty。length()1));StringvaluesScriptSqlScriptUtils。convertForeach(insertSqlProperty,list,(String)null,et,,);StringkeyPropertynull;StringkeyColumnnull;if(tableInfo。havePK()){if(tableInfo。getIdType()IdType。AUTO){keyGeneratorJdbc3KeyGenerator。INSTANCE;keyPropertytableInfo。getKeyProperty();keyColumntableInfo。getKeyColumn();}elseif(null!tableInfo。getKeySequence()){keyGeneratorTableInfoHelper。genKeyGenerator(this。getMethod(sqlMethod),tableInfo,this。builderAssistant);keyPropertytableInfo。getKeyProperty();keyColumntableInfo。getKeyColumn();}}StringsqlString。format(sqlMethod。getSql(),tableInfo。getTableName(),columnScript,valuesScript);SqlSourcesqlSourcethis。languageDriver。createSqlSource(this。configuration,sql,modelClass);returnthis。addInsertMappedStatement(mapperClass,modelClass,this。getMethod(sqlMethod),sqlSource,(KeyGenerator)keyGenerator,keyProperty,keyColumn);}接下来就通过SQL注入器实现真正的批量插入默认的sql注入器publicclassDefaultSqlInjectorextendsAbstractSqlInjector{publicDefaultSqlInjector(){}publicListgetMethodList(Classlt;?mapperClass,TableInfotableInfo){if(tableInfo。havePK()){return(List)Stream。of(newInsert(),newDelete(),newDeleteByMap(),newDeleteById(),newDeleteBatchByIds(),newUpdate(),newUpdateById(),newSelectById(),newSelectBatchByIds(),newSelectByMap(),newSelectCount(),newSelectMaps(),newSelectMapsPage(),newSelectObjs(),newSelectList(),newSelectPage())。collect(Collectors。toList());}else{this。logger。warn(String。format(s,NotfoundTableIdannotation,CannotuseMybatisPlusxxByIdMethod。,tableInfo。getEntityType()));return(List)Stream。of(newInsert(),newDelete(),newDeleteByMap(),newUpdate(),newSelectByMap(),newSelectCount(),newSelectMaps(),newSelectMapsPage(),newSelectObjs(),newSelectList(),newSelectPage())。collect(Collectors。toList());}}}继承DefaultSqlInjector自定义sql注入器authorzhmskydate202281515:13publicclassMySqlInjectorextendsDefaultSqlInjector{OverridepublicListgetMethodList(Classlt;?mapperClass){ListmethodListsuper。getMethodList(mapperClass);更新时自动填充的字段,不用插入值methodList。add(newInsertBatchSomeColumn(ii。getFieldFill()!FieldFill。UPDATE));returnmethodList;}}将自定义的sql注入器注入到Mybatis容器中authorzhmskydate202281515:15ConfigurationpublicclassMybatisPlusConfig{BeanpublicMySqlInjectorsqlInjector(){returnnewMySqlInjector();}}继承BaseMapper添加自定义方法authorzhmskydate202281515:17publicinterfaceCommonMapperTextendsBaseMapperT{真正的批量插入paramentityListreturnintinsertBatchSomeColumn(ListTentityList);}对应的mapper层接口继承上面自定义的mapperauthorzhmskysince20211201MapperpublicinterfaceUserMapperextendsCommonMapperUser{}
最后直接调用UserMapper的insertBatchSomeColumn()方法即可实现真正的批量插入。TestvoidcontextLoads(){for(inti0;i5;i){UserusernewUser();user。setAge(10);user。setUsername(zhmsky);user。setEmail(21575559qq。com);userList。add(user);}longlSystem。currentTimeMillis();userMapper。insertBatchSomeColumn(userList);longl1System。currentTimeMillis();System。out。println(:(l1l));userList。clear();}
查看日志输出信息,观察执行的sql语句;
发现这才是真正意义上的sql层面的批量插入。
但是,到这里并没有结束,mybatisPlus官方提供的insertBatchSomeColumn方法不支持分批插入,也就是有多少直接全部一次性插入,这就可能会导致最后的sql拼接语句特别长,超出了mysql的限制,于是我们还要实现一个类似于saveBatch的分批的批量插入方法。另外,搜索公众号Linux就该这样学后台回复猴子,获取一份惊喜礼包。添加分批插入
模仿原来的saveBatch方法:authorzhmskysince20211201ServicepublicclassUserServiceImplextendsServiceImplUserMapper,UserimplementsUserService{OverrideTransactional(rollbackFor{Exception。class})publicbooleansaveBatch(CollectionUserentityList,intbatchSize){try{intsizeentityList。size();intidxLimitMath。min(batchSize,size);inti1;保存单批提交的数据集合ListUseroneBatchListnewArrayList();for(IteratorUservar7entityList。iterator();var7。hasNext();i){Userelementvar7。next();oneBatchList。add(element);if(iidxLimit){baseMapper。insertBatchSomeColumn(oneBatchList);每次提交后需要清空集合数据oneBatchList。clear();idxLimitMath。min(idxLimitbatchSize,size);}}}catch(Exceptione){log。error(saveBatchfail,e);returnfalse;}returntrue;}}
测试:TestvoidcontextLoads(){for(inti0;i20;i){UserusernewUser();user。setAge(10);user。setUsername(zhmsky);user。setEmail(21575559qq。com);userList。add(user);}longlSystem。currentTimeMillis();userService。saveBatch(userList,10);longl1System。currentTimeMillis();System。out。println(:(l1l));userList。clear();}
输出结果:
分批插入已满足,到此收工结束了。接下来最重要的测试下性能
当前数据表的数据量在100w多条,在此基础上分别拿原始的saveBatch(假的批量插入)和insertBatchSomeColumn(真正的批量插入)进行性能对比(jdbc均开启rewriteBatchedStatements):
原来的假的批量插入:Testvoidinsert(){for(inti0;i50000;i){UserusernewUser();
自定义的insertBatchSomeColumn:TestvoidcontextLoads(){for(inti0;i50000;i){UserusernewUser
分批插入5w条数据,自定义的真正意义上的批量插入耗时减少了3秒左右,用insertBatchSomeColum分批插入1500条数据耗时650毫秒,这速度已经挺快了