Mybatis-Plus saveBatch()批量保存失效的解決
問題
在使用IService.savebatch方法批量插入數(shù)據(jù)時,觀察控制臺打印的Sql發(fā)現(xiàn)并沒有像預想的一樣,而是以逐條方式進行插入,插1000條數(shù)據(jù)就得10s多,正常假如批量插入應該是一條語句:
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)驗,看到最多的是:在JDBC連接串最后添加參數(shù)rewriteBatchedStatements=true,可以大大增加批量插入的效率,加上了發(fā)現(xiàn)還是一條一條插,然后又搜索為什么這個參數(shù)沒用,有說數(shù)據(jù)條數(shù)要>3,這個我肯定滿足,有說JDBC驅動版本問題的,都試了沒用。
多方查詢無果,決定從源碼入手,一步一步跟進看這個saveBatch到底怎么實現(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ù),沒什么好說的,重點看這個executeBatch
2. SqlHelper.java
? ? /**
? ? ?* 執(zhí)行批量操作
? ? ?*
? ? ?* @param entityClass 實體類
? ? ?* @param log ? ? ? ? 日志對象
? ? ?* @param list ? ? ? ?數(shù)據(jù)集合
? ? ?* @param batchSize ? 批次大小
? ? ?* @param consumer ? ?consumer
? ? ?* @param <E> ? ? ? ? T
? ? ?* @return 操作結果
? ? ?* @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 實體
? ? ?* @param log ? ? ? ? 日志對象
? ? ?* @param consumer ? ?consumer
? ? ?* @return 操作結果
? ? ?* @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í)行器切換,當存在批量操作時,會嵌套兩個session的,優(yōu)先commit上一個session
? ? ? ? ? ? //按道理來說,這里的值應該一直為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);
? ? ? ? ? ? //非事物情況下,強制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();
? ? ? ? }
? ? }打斷點發(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后進入了這個doUpdate方法,看了一下,if體內的應該就是批量拼接sql的關鍵,走了幾個循環(huán)發(fā)現(xiàn)我的代碼都是從else體里走了,也就拆成了一條一條的插入語句,那他為什么不進if呢,看了下判斷條件,每次進來。statement都是一個,那問題就出在sql.equals(currentSql) 上面,我比對了下第二個實體的sql和第一個實體的sql,很快就發(fā)現(xiàn)了問題,他們竟然不!一!樣!。
原因是在拼接insert語句時,如果實體的某個屬性值為空,那他將不參與拼接,所以如果你的數(shù)據(jù)null值比較多且比較隨機的分布在各個屬性上,那生成出來的sql就會不一樣,也就沒法走批處理邏輯了。
為了驗證這個發(fā)現(xiàn),我寫了兩段測試代碼比對:
a. list新增三個實體,每個實體在不同的屬性上設置空值
?? ?@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();
? ? }打印結果(三個語句):
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. 還是生成三個實體,但是在相同屬性上設置空值,保證數(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();
?}打印結果(一個語句):
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)
果然,驗證結論正確,實體屬性為null時,會影響生成的插入sql,進而影響批量保存邏輯。
解決方案
定位到了問題,那就也便于解決了,問題原因是生成插入sql時,對null值的處理策略造成的,查閱mybatis-plus官方文檔發(fā)現(xiàn),有一個配置項可以解決這個問題:
insertStrategy
類型:com.baomidou.mybatisplus.annotation.FieldStrategy
默認值:NOT_NULL
字段驗證策略之 insert,在 insert 的時候的字段驗證策略
默認為NOT_NULL就是導致問題的關鍵,改成IGNORED就好了
再查資料發(fā)現(xiàn),在@TableField注解內也可局部制定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;
不管他那套,自己重寫個批量保存方法,自己寫xml拼接sql,簡單粗暴(小心sql超出最大長度)
<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>
到此這篇關于Mybatis-Plus saveBatch()批量保存失效的解決的文章就介紹到這了,更多相關Mybatis-Plus saveBatch()批量保存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java 對 Properties 文件的操作詳解及簡單實例
這篇文章主要介紹了Java 對 Properties 文件的操作詳解及簡單實例的相關資料,需要的朋友可以參考下2017-02-02
Springboot Autowried及Resouce使用對比解析
這篇文章主要介紹了Springboot Autowried及Resouce使用對比解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06
Maven中Could not find artifact XXXX的錯誤解決
本文主要介紹了Maven中Could not find artifact XXXX的錯誤解決,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03
如何從Java環(huán)境中調用GoLang函數(shù)
Go,常被稱為GoLang,Go語言憑借其豐富的標準庫,以及 goroutines和 channels等獨特特性,在開發(fā)可擴展且高效的程序方面展現(xiàn)了顯著優(yōu)勢,許多開發(fā)者傾向于將Go與其他編程語言結合使用,在本文中,我們將深入探討如何從Java環(huán)境中調用GoLang函數(shù),以實現(xiàn)兩種語言的無縫集成2025-01-01
IntelliJ IDEA 創(chuàng)建spring boot 的Hello World 項目(圖解)
這篇文章主要介紹了IntelliJ IDEA 創(chuàng)建spring boot 的Hello World 項目的步驟詳解,需要的朋友可以參考下2018-01-01
Java PriorityQueue優(yōu)點和缺點面試精講
這篇文章主要為大家介紹了Java面試中PriorityQueue的優(yōu)點和缺點及使用注意詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10
Request的包裝類HttpServletRequestWrapper的使用說明
這篇文章主要介紹了Request的包裝類HttpServletRequestWrapper的使用說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08

