记录一下 mybatis-plus + sharding-JDBC 的时候,因为配置多数据源和多个SqlSessionFactory导致 mybatisPlus 执行 saveBatch 异常的问题。
具体异常就是 saveBatch 执行的数据源,与期望的不一致。其实是因为 SqlSessionFactory 错误导致的。
项目中有2个数据源,分别用的不同的 SqlSessionFactory。
第1个 SqlSessionFactory
@Primary
@Bean(name = "myNormalSqlSessionFactory")
public SqlSessionFactory getMybatisSqlSessionFactory(@Qualifier("myNormalDataSource") DataSource myDataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(myDataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 定义多个 sqlSessionFactory 的时候注意 mapper 要指定子目录,否则会 MybatisPlus 会出现 sqlSessionFactory 不正确。
// 原因详见: TableInfoHelper.initTableInfo()
bean.setMapperLocations(resolver.getResources("classpath*:mapper/**/*Mapper.xml"));
bean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-config.xml"));
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(mybatisPlusInterceptor);
bean.setPlugins(interceptors.toArray(new Interceptor[0]));
Properties properties = new Properties();
properties.put("dialect", "mysql");
bean.setConfigurationProperties(properties);
return bean.getObject();
}
第2个 SqlSessionFactory
@Bean(name = "myShardingSqlSessionFactory")
public SqlSessionFactory getMybatisSqlSessionFactory(@Qualifier("myShardingDataSource") DataSource myDataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myDataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 定义多个 sqlSessionFactory 的时候注意 mapper 要指定子目录,否则会 MybatisPlus 会出现 sqlSessionFactory 不正确。
// 原因详见: TableInfoHelper.initTableInfo()
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/**/*Mapper.xml"));
sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-config.xml"));
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(mybatisPlusInterceptor);
sqlSessionFactoryBean.setPlugins(interceptors.toArray(new Interceptor[0]));
Properties properties = new Properties();
properties.put("dialect", "mysql");
sqlSessionFactoryBean.setConfigurationProperties(properties);
return sqlSessionFactoryBean.getObject();
}
启动项目后,执行了 saveBatch()
@GetMapping(value = "/normal/student/insert/batch")
@Transactional(value = "myNormalTransactionManager", rollbackFor = Exception.class)
public String test1() {
List<StudentBase> studentList = StudentUtil.getRandomStudentBaseList(5);
studentNormalService.saveBatch(studentList);
return JSON.toJSONString("ok");
}
注意这里,我用的事务管理器是:myNormalTransactionManager,期望是用第一个 SqlSessionFactory ,即myNormalSqlSessionFactory。
但是执行的时候,用的却是 myShardingSqlSessionFactory (第二个数据源)
我们可以去这类看到 com.baomidou.mybatisplus.extension.toolkit.SqlHelper
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();
//原生无法支持执行器切换,当存在批量操作时,会嵌套两个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");
}
......
经过分析,找下原因:
com.baomidou.mybatisplus.core.metadata.TableInfoHelper 中 initTableInfo方法会将每个 实体类 与对应的 数据库配置保存到 缓存:TABLE_INFO_CACHE 中
也就是。我们在创建 SqlSessionFactory 时候设置的 setMapperLocations, 设置路径下的所有mapper.xml 对应的实体都会保存对应的数据库配置。
因此,我们需要将不同的 SqlSessionFactory 配置,用不同的 mapper 目录来扫描。不同数据源的操作,放在各自的 mapper 子目录下,作区分。
上面我们定义2个SqlSessionFactory中有两句一样的代码:
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/**/*Mapper.xml"));
由于 classpath*:mapper/**/*Mapper.xml 路径一样,导致初始化 实体类和数据库配置对应关系,被覆盖的现象。
即同样的 mapper.xml 文件中被 不同的 SqlSessionFactory 扫描了两次,导致mapper.xml中的实体类信息只有一种SqlSessionFactory信息。
将第1个的 SqlSessionFactory 中这行代码改成:
bean.setMapperLocations(resolver.getResources("classpath*:mapper/normal/**/*Mapper.xml"));
将第2个的 SqlSessionFactory 中这行代码改成:
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/sharding/**/*Mapper.xml"));
重启项目,执行就正常了。