Mybatis-Plus saveBatch()批量保存失效的解決
問題
在使用IService.savebatch方法批量插入數(shù)據(jù)時(shí),觀察控制臺(tái)打印的Sql發(fā)現(xiàn)并沒有像預(yù)想的一樣,而是以逐條方式進(jìn)行插入,插1000條數(shù)據(jù)就得10s多,正常假如批量插入應(yīng)該是一條語句:
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); ...
問題環(huán)境
- jdk 1.8
- spring-boot-starter 2.1.1.RELEASE
- mybatis-plus 3.4.1
- mysql-connector-java 8.0.13
排查過程
先是網(wǎng)上搜索有沒有類似的經(jīng)驗(yàn),看到最多的是:在JDBC連接串最后添加參數(shù)rewriteBatchedStatements=true,可以大大增加批量插入的效率,加上了發(fā)現(xiàn)還是一條一條插,然后又搜索為什么這個(gè)參數(shù)沒用,有說數(shù)據(jù)條數(shù)要>3,這個(gè)我肯定滿足,有說JDBC驅(qū)動(dòng)版本問題的,都試了沒用。
多方查詢無果,決定從源碼入手,一步一步跟進(jìn)看這個(gè)saveBatch到底怎么實(shí)現(xià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ù),沒什么好說的,重點(diǎn)看這個(gè)executeBatch
2. SqlHelper.java
? ? /**
? ? ?* 執(zhí)行批量操作
? ? ?*
? ? ?* @param entityClass 實(shí)體類
? ? ?* @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();
? ? ? ? ? ? //原生無法支持執(zhí)行器切換,當(dāng)存在批量操作時(shí),會(huì)嵌套兩個(gè)session的,優(yōu)先commit上一個(gè)session
? ? ? ? ? ? //按道理來說,這里的值應(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)過一次consumer.accept(sqlSession),就打印一行insert語句出來,看看里面搞了什么鬼
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體里走了,也就拆成了一條一條的插入語句,那他為什么不進(jìn)if呢,看了下判斷條件,每次進(jìn)來。statement都是一個(gè),那問題就出在sql.equals(currentSql) 上面,我比對(duì)了下第二個(gè)實(shí)體的sql和第一個(gè)實(shí)體的sql,很快就發(fā)現(xiàn)了問題,他們竟然不!一!樣!。
原因是在拼接insert語句時(shí),如果實(shí)體的某個(gè)屬性值為空,那他將不參與拼接,所以如果你的數(shù)據(jù)null值比較多且比較隨機(jī)的分布在各個(gè)屬性上,那生成出來的sql就會(huì)不一樣,也就沒法走批處理邏輯了。
為了驗(yàn)證這個(gè)發(fā)現(xiàn),我寫了兩段測(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è)語句):
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è)語句):
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)而影響批量保存邏輯。
解決方案
定位到了問題,那就也便于解決了,問題原因是生成插入sql時(shí),對(duì)null值的處理策略造成的,查閱mybatis-plus官方文檔發(fā)現(xiàn),有一個(gè)配置項(xiàng)可以解決這個(gè)問題:
insertStrategy
類型:com.baomidou.mybatisplus.annotation.FieldStrategy
默認(rèn)值:NOT_NULL
字段驗(yàn)證策略之 insert,在 insert 的時(shí)候的字段驗(yàn)證策略
默認(rèn)為NOT_NULL就是導(dǎo)致問題的關(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;
不管他那套,自己重寫個(gè)批量保存方法,自己寫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)文章
Mybatis常用分頁插件實(shí)現(xiàn)快速分頁處理技巧
這篇文章主要介紹了Mybatis常用分頁插件實(shí)現(xiàn)快速分頁處理的方法。非常不錯(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-08
idea項(xiàng)目中target文件提示拒絕訪問的解決
這篇文章主要介紹了idea項(xiàng)目中target文件提示拒絕訪問的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
Java連接MYSQL數(shù)據(jù)庫的實(shí)現(xiàn)步驟
以下的文章主要描述的是java連接MYSQL數(shù)據(jù)庫的正確操作步驟,在此篇文章里我們主要是以實(shí)例列舉的方式來引出其具體介紹2013-06-06

