MybatisPlus+ShardingJDBC批量插入返回主键为空(附源码解析)

项目背景:

项目采用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,不需要的就直接
使用默认配配置的数据源即可。


版权声明:本文为weixin_39893621原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。