项目背景:
项目采用SpringBoot+Mybatis-Plus+ShardingJDBC进行数据库插入操作,ShardingJDBC使用了其分库分表、读写分离及影子库的功能。
问题发现:
在一次新增操作时,t1表新增或更新多条数据后t2表需根据t1的主键先删除表中数据后再进行批量插入新的配置数据,通过断点发现t1表插入成功后未返回主键ID。 贴上涉事代码块:
List<MemberRightShowConfig> memberRightShowConfigList = mrsRightDetailVO.getShowConfigs().parallelStream()
....
.collect(Collectors.toList());
.....
saveOrUpdateShowConfigs = memberRightShowConfigService.saveBatch(memberRightShowConfigList); ...
for (int i = 0; i < memberRightShowConfigList.size(); i++) {
MemberRightShowConfig memberRightShowConfig = memberRightShowConfigList.get(i);
Long showConfigId = memberRightShowConfig.getId()
...
memberRightTemplateImplService
.remove(new QueryWrapper<MemberRightTemplateImpl>()
.eq("show_config_id", showConfigId));
}
问题分析:
猜想1:因事务导致的批量插入不返回主键
猜想验证:
取消事务进行插入后,仍出现主键不返回的相同问题
猜想2:mybatis-plus的saveBatch方法不返回主键ID
猜想验证:
跟踪mybatis-plus的saveBatch方法 mybatis-plus的saveBatch方法首先进入的
package com.baomidou.mybatisplus.extension.service;
public interface IService<T> {
@Transactional(rollbackFor = Exception.class)
default boolean saveBatch(Collection<T> entityList) {
return saveBatch(entityList, DEFAULT_BATCH_SIZE);
}
}
具体实现
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = sqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize,
(sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
跟踪sqlSession.insert方法,这个方法有三个实现DefaultSqlSession、SqlSessionManager、SqlSessionTemplate,根据定位走了DefaultSqlSession的实现:
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
...
@Override
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
executor.update(ms, wrapCollection(parameter))这个方法有3个实现方法:BaseExecutor,CachingExecutor,MybatisCachingExecutor
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource())
.activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
这个doUpdate方法有BatchExecutor,ReuseExecutor,SimpleExecutor,ClosedExecutor,MybatisBatchExecutor,MybatisReuseExecutor,MybatisSimpleExecutor
这几种实现。Mybatis开头的是MybatisPlus提供的实现,分别对应mybatis的simple,reuse,batch执行器类别。不管哪个执行器,里面都会有一个StatementHandler接口来负责
具体实现。
StatementHandler接口有BaseStatementHandler,CallableStatementHandler,PreparedStatementHandler,RoutingStatementHandler和SimpleStatementHandler几种实现。
插入,批量插入分别对应update,batch方法。我们使用了PreparedPool,所以进入了PreparedStatementHandler实现。
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
@Override
public void batch(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.addBatch();
}
可以发现在进行单个插入时会有keyGenerator来执行插入后返回主键的操作,这个keyGenerator在mybatis中默认是Jdbc3KeyGenerator。
但是走到batch方法后,进入addBatch,这个addBatch属于PreparedStatement接口中的方法。实现有很多就不一一列举了,因为使用了shardingJDBC所以进入了
ShardingSpherePreparedStatement
@Override
public void addBatch() {
try {
executionContext = createExecutionContext(createLogicSQL());
batchPreparedStatementExecutor.addBatchForExecutionUnits(executionContext.getExecutionUnits());
} finally {
currentResultSet = null;
clearParameters();
}
}
查看createExecutionContext这个方法
private ExecutionContext createExecutionContext(final LogicSQL logicSQL) {
SQLCheckEngine.check(logicSQL.getSqlStatementContext().getSqlStatement(), logicSQL.getParameters(),
metaDataContexts.getMetaData(
connection.getSchema()).getRuleMetaData().getRules(), connection.getSchema(),
metaDataContexts.getMetaDataMap(), null);
ExecutionContext result = kernelProcessor.generateExecutionContext(
logicSQL,
metaDataContexts.getMetaData(connection.getSchema()),
metaDataContexts.getProps());
findGeneratedKey(result)
.ifPresent(generatedKey ->
generatedValues.addAll(generatedKey.getGeneratedValues()));
return result;
}
这里面有一个findGeneratorKey的方法,在这里会根据配置文件中对应表配置的keyGenerator算法进行配置。创建完ExecutionContext的后就包含了keyGenerator的配置,因为
这里我们没有配置,所以keyGenerator就是空的。看到这里就知道为什么返回List中所有对象的主键ID都是空的了,因为没有设置keyGenerator也没有走默认的keyGenerator。
其实想想也很正常,如果插入的列表里面含有不同库不同表的数据,那么最终返回的主键是什么呢?就有可能存在主键一致数据不同的对象,这样会不会就有了混淆,所以
shardingJDBC要求对接入的分表要有主键生成的策略就在这里。单条数据插入还不会有主键重复的情况出现,但是多条就有可能出现主键重复,这是不允许的。
总结
如果使用shardingJDBC进行批量插入的话一定要设置keyGenerator的算法。
对于单库单表尽量就不要用shardingJDBC,所以最后我们的解决方案是添加了dynamic这个组件动态切换数据源,需要用到shardingJDBC的走shardingJDBC,不需要的就直接
使用默认配配置的数据源即可。