mybatis-plus是一个很好的mybatis增强工具,主要因为其CRUD操作可以减少很多代码的编写,提高开发效率,因此引入了现有项目。有兴趣同学可以查看官方文档学习:https://baomidou.com/
0. 背景介绍
- 项目是springboot 2.1.6项目,且有多个数据源,每个数据源都需要spring事务管理
- 本篇基于mybatis-plus 3.3.0
- 如下为其中一个数据源的事务管理配置
@Configuration
@MapperScan(basePackages = "com.zeng.reach.mapper.test", sqlSessionTemplateRef = "sqlSessionTemplateTest")
public class TestDataConfig {
private static final Logger LOG = LoggerFactory.getLogger(TestDataConfig.class);
@Bean(name = "dataSourceTest", initMethod = "init", destroyMethod = "close")
@Primary
@ConfigurationProperties(prefix = "test.datasource")
public DruidDataSource dataSourceTest() {
return new DruidDataSource();
}
@Bean
public GlobalConfig globalConfig() {
GlobalConfig conf = new GlobalConfig();
conf.setDbConfig(new GlobalConfig.DbConfig().setKeyGenerator(new OracleKeyGenerator()));
return conf;
}
@Bean(name = "sqlSessionFactoryTest")
@Primary
@DependsOn("globalConfig")
public MybatisSqlSessionFactoryBean sqlSessionFactoryTest(@Qualifier("dataSourceTest") DruidDataSource dataSource) throws IOException {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources("classpath*:mapper/test/*Mapper.xml"));
return bean;
}
@Bean(name = "sqlSessionTemplateTest")
@Primary
public SqlSessionTemplate sqlSessionTemplateTest(@Qualifier("sqlSessionFactoryTest") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = {"transactionManagerTest", "test"})
@Primary
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dataSourceTest") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
1. 问题描述
插入数据时,抛出异常,异常堆栈(有截取)如下:
2020-11-30 10:14:41:418[DEBUG][druid.sql.Statement][18a88c4d-cba6-4adb-af08-a5f1090c955a]-{conn-10010, pstmt-20001} created. INSERT INTO tbl_task ( id,
no,
receive_time,
min,
code,
success_status,
status,
info,
source ) VALUES ( ?,
?,
?,
?,
?,
?,
?,
?,
? )
2020-11-30 10:14:41:420[DEBUG][com.zeng.reach.mapper.test.TaskMapper.insert][18a88c4d-cba6-4adb-af08-a5f1090c955a]-==> Parameters: 0(Long), 11111111(String), 2020-11-30 10:14:38.74(Timestamp), 1(Integer), box-1222(String), 1(Integer), 1(Integer), dffsf(String), dsfs(String)
java.sql.SQLException: ORA-00001: 违反唯一约束条件 (TEST.PK_TBL_TASK_ID)
at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:445)
at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:396)
at oracle.jdbc.driver.T4C8Oall.processError(T4C8Oall.java:879)
at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:450)
at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:192)
at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:531)
我们可以看到打印的sql,主键id为0,经过几次测试,都是0。主键是oracle的序列,所以确定是序列没有获取到。
2. 解决方案
@Configuration
@MapperScan(basePackages = "com.zeng.reach.mapper.test", sqlSessionTemplateRef = "sqlSessionTemplateTest")
public class TestDataConfig {
private static final Logger LOG = LoggerFactory.getLogger(TestDataConfig.class);
@Bean(name = "dataSourceTest", initMethod = "init", destroyMethod = "close")
@Primary
@ConfigurationProperties(prefix = "test.datasource")
public DruidDataSource dataSourceTest() {
return new DruidDataSource();
}
@Bean
public GlobalConfig globalConfig() {
GlobalConfig conf = new GlobalConfig();
conf.setDbConfig(new GlobalConfig.DbConfig().setKeyGenerator(new OracleKeyGenerator()));
return conf;
}
@Bean(name = "sqlSessionFactoryTest")
@Primary
@DependsOn("globalConfig")
public MybatisSqlSessionFactoryBean sqlSessionFactoryTest(@Qualifier("dataSourceTest") DruidDataSource dataSource) throws IOException {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources("classpath*:mapper/test/*Mapper.xml"));
//注意:只增加了这一行
bean.setGlobalConfig(globalConfig());
return bean;
}
@Bean(name = "sqlSessionTemplateTest")
@Primary
public SqlSessionTemplate sqlSessionTemplateTest(@Qualifier("sqlSessionFactoryTest") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = {"transactionManagerTest", "test"})
@Primary
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dataSourceTest") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
3、 分析原因
-
按照官方配置,配置了全局配置bean: GlobalConfig,如上背景交代。
-
经过上面异常分析,猜测GlobalConfig中OracleKeyGenerator是否配置有问题。
-
调试源码OracleKeyGenerator,如下(debug发现下面的代码没有执行):
public class OracleKeyGenerator implements IKeyGenerator {
@Override
public String executeSql(String incrementerName) {
return "SELECT " + incrementerName + ".NEXTVAL FROM DUAL";
}
}
- 反向查询调用链,到Insert源码中,如下分析:
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = new NoKeyGenerator();
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
String columnScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
String valuesScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlPropertyMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
if (tableInfo.getIdType() == IdType.AUTO) {
/** 自增主键 */
keyGenerator = new Jdbc3KeyGenerator();
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
//如果我们实体映射的tableInfo中设置的@KeySequence不为空,此处即可获取项目中配置的OracleKeyGenerator(但此处,我们发现null == tableInfo.getKeySequence())
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
}
-
经过调试,我们最终确定tableInfo中的KeySequence,是从MybatisSqlSessionFactoryBean的GlobalConfig中DbConfig中IKeyGenerator中获取。(其实从官网GlobalConfig配置Bean的代码中也可以看出来)
-
但是我们是直接用@Bean的方式去实例化GlobalConfig,而MybatisSqlSessionFactoryBean中需要以set方式设置GlobalConfig,所以需要我们手动设置GlobalConfig。com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean源码如下:
public class MybatisSqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> { private static final Logger LOGGER = LoggerFactory.getLogger(MybatisSqlSessionFactoryBean.class); // TODO 自定义枚举包 @Setter private String typeEnumsPackage; // TODO 自定义全局配置 @Setter private GlobalConfig globalConfig; }
4. 总结
- 由于连接多个数据源,需要事务管理;没有采用mybatis-plus推荐的springboot配置方式,而是使用的springmvc方式。这种方式会导致MybatisSqlSessionFactoryBean中增强的属性值都需要手动编码set设值进去
- 如果没有用mybatis-plus springboot自动装配的SqlSessionFactory,通过application.yml配置的MybatisProperties属性都是无效的。如下MybatisAutoConfiguration源码中:sqlSessionFactory方法不会执行,其中的factory.setxxx设置自然都是不会执行;sqlSessionTemplate方法同理。这就是有的同学疑惑为什么配置了application.yml但没有生效的原因,有兴趣可以去了解springboot starter原理(有空写一篇解释)
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
private final MybatisProperties properties;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
Configuration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new Configuration();
}
if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
customizer.customize(configuration);
}
}
factory.setConfiguration(configuration);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}
}