Mybatis-Plus saveBatch()批量保存失效的解决
目录
- 问题
- 问题环境
- 排查过程
- 解决方案
问题
在使用IService.saveBATch方法批量插入数据时,观察控制台打印的Sql发现并没有像预想的一样,而是以逐条方式进行插入,插1000条数据就得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); ...
问题环境
- jdk 1.8
- spring-boot-starter 2.1.1.RELEASE
- mybatis-plus 3.4.1
- mysql-connector-Java 8.0.13
排查过程
先是网上搜索有没有类似的经验,看到最多的是:在JDBC连接串最后添加参数rewriteBatchedStatements=true,可以大大增加批量插入的效率,加上了发现还是一条一条插,然后又搜索为什么这个参数没用,有说数据条数要>3,这个我肯定满足,有说JDBC驱动版本问题的,都试了没用。
多方查询无果,决定从源码入手,一步一步编程客栈跟进看这个saveBatch到底怎么实现的,在哪一步出了问题。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)); }
入口函数,没什么好说的,重点看这个executeBatch
2. SqlHelper.java
/** * 执行批量操作 * * @param entityClass 实体类 * @param log 日志对象 * @param list 数据集合 * @param batchSize 批次大小 * @param consumer consumer *开发者_Python @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++; } }); } /** * 执行批量操作 * * @param entityClass 实体 * @param log 日志对象 * @param consumer consumer * @return 操作结果 * @since 3.4.0 */ public static boolean executeBatch(Class<?> entityClass, Log log, Consumer<SqlSession&gHVeIMGt; consumer) { SqlSessionFactory sqlSessionFactory = sqlSessionFactory(entityClass); SqlSessionHolder sqlSessionHolder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sqlSessionFactory); boolean transaction = TransactionSynchronizationManager.isSynchronizationActive(); if (sqlSessionHolder != null) { SqlSession sqlSession = sqlSessionHolder.getSqlSession(); //原生无法支持执行器切换,当存在批量操作时,会嵌套两个session的,优先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(); } }
打断点发现,每经过一次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); batchRandroidesult.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的关键,走了几个循环发现我的代码都是从else体里走了,也就拆成了一条一条的插入语句,那他为什么不进if呢,看了下判断条件,每次进来。statement都是一个,那问题就出在sql.equals(currentSql) 上面,我比对了下第二个实体的sql和第一个实体的sql,很快就发现了问题,他们竟然不!一!样!。
原因是在拼接insert语句时,如果实体的某个属性值为空,那他将不参与拼接,所以如果你的数据null值比较多且比较随机的分布在各个属性上,那生成出来的sql就会不一样,也就没法走批处理逻辑了。为了验证这个发现,我写了两段测试代码比对:
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); listHVeIMG.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. 还是生成三个实体,但是在相同属性上设置空值,保证数据格式一致性
@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官方文档发现,有一个配置项可以解决这个问题:
insertStrategy
类型:com.baomidou.mybatisplus.annotation.FieldStrategy默认值:NOT_NULL字段验证策略之 insert,在 insert 的时候的字段验证策略
默认为NOT_NULL就是导致问题的关键,改成IGNORED就好了
再查资料发现,在@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()批量保存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!
精彩评论