Mybatis-Plus saveBatch()批量保存失效的解決
問(wèn)題
在使用IService.savebatch方法批量插入數(shù)據(jù)時(shí),觀察控制臺(tái)打印的Sql發(fā)現(xiàn)并沒(méi)有像預(yù)想的一樣,而是以逐條方式進(jìn)行插入,插1000條數(shù)據(jù)就得10s多,正常假如批量插入應(yīng)該是一條語(yǔ)句:
insert table (field1, field2) values (val1, val2), (val3, val4), (val5, val6), ... ;
而我的是這樣:
insert table (field1, field2) values (val1, val2); insert table (field1, field2) values (val3, val4); ...
問(wèn)題環(huán)境
- jdk 1.8
- spring-boot-starter 2.1.1.RELEASE
- mybatis-plus 3.4.1
- mysql-connector-java 8.0.13
排查過(guò)程
先是網(wǎng)上搜索有沒(méi)有類(lèi)似的經(jīng)驗(yàn),看到最多的是:在JDBC連接串最后添加參數(shù)rewriteBatchedStatements=true,可以大大增加批量插入的效率,加上了發(fā)現(xiàn)還是一條一條插,然后又搜索為什么這個(gè)參數(shù)沒(méi)用,有說(shuō)數(shù)據(jù)條數(shù)要>3,這個(gè)我肯定滿(mǎn)足,有說(shuō)JDBC驅(qū)動(dòng)版本問(wèn)題的,都試了沒(méi)用。
多方查詢(xún)無(wú)果,決定從源碼入手,一步一步跟進(jìn)看這個(gè)saveBatch到底怎么實(shí)現(xiàn)的,在哪一步出了問(wèn)題。
1.ServiceImpl.java
? ? /** ? ? ?* 批量插入 ? ? ?* ? ? ?* @param entityList ignore ? ? ?* @param batchSize ?ignore ? ? ?* @return ignore ? ? ?*/ ? ? @Transactional(rollbackFor = Exception.class) ? ? @Override ? ? public boolean saveBatch(Collection<T> entityList, int batchSize) { ? ? ? ? String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE); ? ? ? ? return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); ? ? }
入口函數(shù),沒(méi)什么好說(shuō)的,重點(diǎn)看這個(gè)executeBatch
2. SqlHelper.java
? ? /** ? ? ?* 執(zhí)行批量操作 ? ? ?* ? ? ?* @param entityClass 實(shí)體類(lèi) ? ? ?* @param log ? ? ? ? 日志對(duì)象 ? ? ?* @param list ? ? ? ?數(shù)據(jù)集合 ? ? ?* @param batchSize ? 批次大小 ? ? ?* @param consumer ? ?consumer ? ? ?* @param <E> ? ? ? ? T ? ? ?* @return 操作結(jié)果 ? ? ?* @since 3.4.0 ? ? ?*/ ? ? 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++; ? ? ? ? ? ? } ? ? ? ? }); ? ? } ?/** ? ? ?* 執(zhí)行批量操作 ? ? ?* ? ? ?* @param entityClass 實(shí)體 ? ? ?* @param log ? ? ? ? 日志對(duì)象 ? ? ?* @param consumer ? ?consumer ? ? ?* @return 操作結(jié)果 ? ? ?* @since 3.4.0 ? ? ?*/ ? ? public static boolean executeBatch(Class<?> entityClass, Log log, Consumer<SqlSession> consumer) { ? ? ? ? SqlSessionFactory sqlSessionFactory = sqlSessionFactory(entityClass); ? ? ? ? SqlSessionHolder sqlSessionHolder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sqlSessionFactory); ? ? ? ? boolean transaction = TransactionSynchronizationManager.isSynchronizationActive(); ? ? ? ? if (sqlSessionHolder != null) { ? ? ? ? ? ? SqlSession sqlSession = sqlSessionHolder.getSqlSession(); ? ? ? ? ? ? //原生無(wú)法支持執(zhí)行器切換,當(dāng)存在批量操作時(shí),會(huì)嵌套兩個(gè)session的,優(yōu)先commit上一個(gè)session ? ? ? ? ? ? //按道理來(lái)說(shuō),這里的值應(yīng)該一直為false。 ? ? ? ? ? ? sqlSession.commit(!transaction); ? ? ? ? } ? ? ? ? SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); ? ? ? ? if (!transaction) { ? ? ? ? ? ? log.warn("SqlSession [" + sqlSession + "] was not registered for synchronization because DataSource is not transactional"); ? ? ? ? } ? ? ? ? try { ? ? ? ? ? ? consumer.accept(sqlSession); ? ? ? ? ? ? //非事物情況下,強(qiáng)制commit。 ? ? ? ? ? ? sqlSession.commit(!transaction); ? ? ? ? ? ? return true; ? ? ? ? } catch (Throwable t) { ? ? ? ? ? ? sqlSession.rollback(); ? ? ? ? ? ? Throwable unwrapped = ExceptionUtil.unwrapThrowable(t); ? ? ? ? ? ? if (unwrapped instanceof RuntimeException) { ? ? ? ? ? ? ? ? MyBatisExceptionTranslator myBatisExceptionTranslator ? ? ? ? ? ? ? ? ? ? = new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true); ? ? ? ? ? ? ? ? throw Objects.requireNonNull(myBatisExceptionTranslator.translateExceptionIfPossible((RuntimeException) unwrapped)); ? ? ? ? ? ? } ? ? ? ? ? ? throw ExceptionUtils.mpe(unwrapped); ? ? ? ? } finally { ? ? ? ? ? ? sqlSession.close(); ? ? ? ? } ? ? }
打斷點(diǎn)發(fā)現(xiàn),每經(jīng)過(guò)一次consumer.accept(sqlSession),就打印一行insert語(yǔ)句出來(lái),看看里面搞了什么鬼
3. MybatisBatchExecutor.java
@Override ? ? public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { ? ? ? ? final Configuration configuration = ms.getConfiguration(); ? ? ? ? final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null); ? ? ? ? final BoundSql boundSql = handler.getBoundSql(); ? ? ? ? final String sql = boundSql.getSql(); ? ? ? ? final Statement stmt; ? ? ? ? if (sql.equals(currentSql) && ms.equals(currentStatement)) { ? ? ? ? ? ? int last = statementList.size() - 1; ? ? ? ? ? ? stmt = statementList.get(last); ? ? ? ? ? ? applyTransactionTimeout(stmt); ? ? ? ? ? ? handler.parameterize(stmt);//fix Issues 322 ? ? ? ? ? ? BatchResult batchResult = batchResultList.get(last); ? ? ? ? ? ? batchResult.addParameterObject(parameterObject); ? ? ? ? } else { ? ? ? ? ? ? Connection connection = getConnection(ms.getStatementLog()); ? ? ? ? ? ? stmt = handler.prepare(connection, transaction.getTimeout()); ? ? ? ? ? ? if (stmt == null) { ? ? ? ? ? ? ? ? return 0; ? ? ? ? ? ? } ? ? ? ? ? ? handler.parameterize(stmt); ? ?//fix Issues 322 ? ? ? ? ? ? currentSql = sql; ? ? ? ? ? ? currentStatement = ms; ? ? ? ? ? ? statementList.add(stmt); ? ? ? ? ? ? batchResultList.add(new BatchResult(ms, sql, parameterObject)); ? ? ? ? } ? ? ? ? handler.batch(stmt); ? ? ? ? return BATCH_UPDATE_RETURN_VALUE; ? ? }
一頓Step Into后進(jìn)入了這個(gè)doUpdate方法,看了一下,if體內(nèi)的應(yīng)該就是批量拼接sql的關(guān)鍵,走了幾個(gè)循環(huán)發(fā)現(xiàn)我的代碼都是從else體里走了,也就拆成了一條一條的插入語(yǔ)句,那他為什么不進(jìn)if呢,看了下判斷條件,每次進(jìn)來(lái)。statement都是一個(gè),那問(wèn)題就出在sql.equals(currentSql) 上面,我比對(duì)了下第二個(gè)實(shí)體的sql和第一個(gè)實(shí)體的sql,很快就發(fā)現(xiàn)了問(wèn)題,他們竟然不!一!樣!。
原因是在拼接insert語(yǔ)句時(shí),如果實(shí)體的某個(gè)屬性值為空,那他將不參與拼接,所以如果你的數(shù)據(jù)null值比較多且比較隨機(jī)的分布在各個(gè)屬性上,那生成出來(lái)的sql就會(huì)不一樣,也就沒(méi)法走批處理邏輯了。
為了驗(yàn)證這個(gè)發(fā)現(xiàn),我寫(xiě)了兩段測(cè)試代碼比對(duì):
a. list新增三個(gè)實(shí)體,每個(gè)實(shí)體在不同的屬性上設(shè)置空值
?? ?@Autowired ? ? private IBPModelService modelService; ?? ?@PostMapping("/save") ? ? @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) ? ? public R testSaveBatch() { ? ? ? ? BPModel model_1 = new BPModel(); ? ? ? ? model_1.setModelName("模型1"); ? ? ? ? BPModel model_2 = new BPModel(); ? ? ? ? model_2.setContent("模型2 content"); ? ? ? ? BPModel model_3 = new BPModel(); ? ? ? ? model_3.setModelDesc("模型3 desc"); ? ? ? ? List<BPModel> list = new ArrayList<>(); ? ? ? ? list.add(model_1); ? ? ? ? list.add(model_2); ? ? ? ? list.add(model_3); ? ? ? ? modelService.saveBatch(list); ? ? ? ? return R.ok(); ? ? }
打印結(jié)果(三個(gè)語(yǔ)句):
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@75dbdb41] will be managed by Spring
==> Preparing: INSERT INTO BP_MODEL ( model_name ) VALUES ( ? )
==> Parameters: 模型1(String)
==> Preparing: INSERT INTO BP_MODEL ( content ) VALUES ( ? )
==> Parameters: 模型2 content(String)
==> Preparing: INSERT INTO BP_MODEL ( model_desc ) VALUES ( ? )
==> Parameters: 模型3 desc(String)
b. 還是生成三個(gè)實(shí)體,但是在相同屬性上設(shè)置空值,保證數(shù)據(jù)格式一致性
?@PostMapping("/save") ?@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) ?public R testSaveBatch() { ? ? ? ? BPModel model_1 = new BPModel(); ? ? ? ? model_1.setModelName("模型1"); ? ? ? ? BPModel model_2 = new BPModel(); ? ? ? ? model_2.setModelName("模型2"); ? ? ? ? BPModel model_3 = new BPModel(); ? ? ? ? model_3.setModelName("模型3"); ? ? ? ? List<BPModel> list = new ArrayList<>(); ? ? ? ? list.add(model_1); ? ? ? ? list.add(model_2); ? ? ? ? list.add(model_3); ? ? ? ? modelService.saveBatch(list); ? ? ? ? return R.ok(); ?}
打印結(jié)果(一個(gè)語(yǔ)句):
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@6e4b5fc7] will be managed by Spring
==> Preparing: INSERT INTO BP_MODEL ( model_name ) VALUES ( ? )
==> Parameters: 模型1(String)
==> Parameters: 模型2(String)
==> Parameters: 模型3(String)
果然,驗(yàn)證結(jié)論正確,實(shí)體屬性為null時(shí),會(huì)影響生成的插入sql,進(jìn)而影響批量保存邏輯。
解決方案
定位到了問(wèn)題,那就也便于解決了,問(wèn)題原因是生成插入sql時(shí),對(duì)null值的處理策略造成的,查閱mybatis-plus官方文檔發(fā)現(xiàn),有一個(gè)配置項(xiàng)可以解決這個(gè)問(wèn)題:
insertStrategy
類(lèi)型:com.baomidou.mybatisplus.annotation.FieldStrategy
默認(rèn)值:NOT_NULL
字段驗(yàn)證策略之 insert,在 insert 的時(shí)候的字段驗(yàn)證策略
默認(rèn)為NOT_NULL就是導(dǎo)致問(wèn)題的關(guān)鍵,改成IGNORED就好了
再查資料發(fā)現(xiàn),在@TableField注解內(nèi)也可局部制定insertStrategy屬性, 那解決方案就比較多樣化了:
全局配置insertStrategy為IGNORED
# mybatis 全局配置 mybatis-plus: ? mapper-locations: classpath:mapper/*.xml ? global-config: ? ? db-config: ? ? ? id-type: auto ? ? ? insert-strategy: ignored ? configuration: ? ? map-underscore-to-camel-case: true ? ? call-setters-on-nulls: true
為可能受影響的屬性添加注解
@TableField(insertStrategy = FieldStrategy.IGNORED) private String content;
不管他那套,自己重寫(xiě)個(gè)批量保存方法,自己寫(xiě)xml拼接sql,簡(jiǎn)單粗暴(小心sql超出最大長(zhǎng)度)
<insert id="insertBatch" parameterType="java.util.List"> insert into table_name (id,code,name,content) VALUES <foreach collection ="list" item="entity" index= "index" separator =","> ( #{entity.id}, #{entity.code}, #{entity.name}, #{entity.content} ) </foreach> </insert>
到此這篇關(guān)于Mybatis-Plus saveBatch()批量保存失效的解決的文章就介紹到這了,更多相關(guān)Mybatis-Plus saveBatch()批量保存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中的靜態(tài)內(nèi)部類(lèi)詳解及代碼示例
這篇文章主要介紹了Java中的靜態(tài)內(nèi)部類(lèi)詳解及代碼示例,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10Mybatis常用分頁(yè)插件實(shí)現(xiàn)快速分頁(yè)處理技巧
這篇文章主要介紹了Mybatis常用分頁(yè)插件實(shí)現(xiàn)快速分頁(yè)處理的方法。非常不錯(cuò)具有參考借鑒價(jià)值,感興趣的朋友一起看看2016-10-10基于java springboot + mybatis實(shí)現(xiàn)電影售票管理系統(tǒng)
這篇文章主要介紹了基于java springboot + mybatis實(shí)現(xiàn)的完整電影售票管理系統(tǒng)基于java springboot + mybatis,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08idea項(xiàng)目中target文件提示拒絕訪問(wèn)的解決
這篇文章主要介紹了idea項(xiàng)目中target文件提示拒絕訪問(wèn)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11Java連接MYSQL數(shù)據(jù)庫(kù)的實(shí)現(xiàn)步驟
以下的文章主要描述的是java連接MYSQL數(shù)據(jù)庫(kù)的正確操作步驟,在此篇文章里我們主要是以實(shí)例列舉的方式來(lái)引出其具體介紹2013-06-06