一、说明

文章中写的东西不一定是完全正确的,希望看到这篇文章的同学也可以自己上手试试,如有大佬为我指正错误,万分感谢。该文章算是记录自己折腾这个东西的一个过程,给大家分享一下过程,希望可以帮到你们一点小忙,同时往后可以给自己复习一下。第一次写文章,写的不好的地方也希望大家多多包涵,接受一切指正。

感谢以下文章给予我解决思路,感谢各位大佬。

SpringBoot中mybatis多数据源配置自动转换驼峰标识没有生效_mybatis下划线转驼峰配置不生效-CSDN博客

SpringBoot+MyBatis项目中同时操作多个数据库_springboot mybatis连接多个数据库-CSDN博客

springboot+mybatis+mysql+yml配置多数据源_mysql datasoure yml-CSDN博客

咱们从头到尾说一次 Spring 事务管理(器) – 掘金 (juejin.cn)

SpringBoot整合mybatis开启驼峰命名 – 简书

二、业务场景

写毕设项目的时候想要一个新增比赛的功能,大致如下:新增比赛信息,把数据存入到比赛信息表,用户可以报名比赛,用户绑定指定比赛。因为比赛不止一个,如果把参赛者的信息全部放在一个表,那以后对数据导出、查找可能存在一定影响。

那么我能不能为每一个比赛,新建一个表作为存放参赛者信息的数据表。那么就需要在我新建比赛的时候,需要将比赛信息存入比赛信息表,同时创建一张表用来存储参赛者。由于当前数据库存放着系统的权限等信息,我不想把这个数据库弄得很乱,所以需要新建一个数据库,用来专门存放这些表。结构大致如图所示:

三、数据源配置

单数据源的配置已经做过很多次了,那么多数据源应该怎样去配置呢?经过一番面向百度、AI编程以后,有了大致的头绪,参考多数文章,经过不断尝试整出了一个能跑动的配置方案。

使用yml格式的配置文件进行配置,单数据源配置样式如下:

spring:datasource:url:driver-class-name:username:password:

多数据源配置样式如下:

spring: datasource:yxmy-system:driver-class-name: ${yxmy.datasource.driver-class-name}url: jdbc:mysql://${yxmy.datasource.host}:${yxmy.datasource.port}/${yxmy.datasource.system-database}" />

mapper-locations 为 你项目中写的xml文件存放位置,type-aliases-package为项目中实体类的位置。

注意:如果你配置的目录为空目录,idea可能会排除该目录不会加到target中,导致项目启动的时候会扫描不到该目录,提示你配置路径错误。可以在目录中新建一个文件放着,或者在classpath后面加个*号,eg:mapper-location: classpath*:..........,详细信息可以搜索classpath 和 classpath* 的区别。

此时yml中的配置就结束了,在springboot项目中,spring会为我们自动配置数据源,当我们设置多个数据源的时候,建议将数据源自动配置关闭(试了一下不关好像影响也不太大,但是没有追代码,不知道具体是怎样加载的)

在启动类中的@SpringBootApplication注解加入参数,同时加入@MapperScan 指明mapper接口文件所在位置:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})@MapperScan("com.yxmy.mapper")

创建数据源配置类,在我的项目中存在两个数据源,一个为system,一个为competition创建如下配置类:

system

@Configuration@MapperScan(basePackages = "com.yxmy.mapper.system", sqlSessionFactoryRef = "systemSqlSessionFactory")public class SystemDataSourceConfig {@Value("${spring.datasource.yxmy-system.url}")private String url;@Value("${spring.datasource.yxmy-system.username}")private String username;@Value("${spring.datasource.yxmy-system.password}")private String password;@Value("${spring.datasource.yxmy-system.mapper-locations}")private String mapperLocations;@Value("${spring.datasource.yxmy-system.driver-class-name}")private String driverClassName;@Value("${spring.datasource.yxmy-system.type-aliases-package}")private String typeAliasesPackage;@Primary@Bean(name = "systemDataSource")public DruidDataSource setDataSource() {DruidDataSource dataSource = new DruidDataSource();dataSource.setUrl(url);dataSource.setUsername(username);dataSource.setPassword(password);dataSource.setDriverClassName(driverClassName);return dataSource;}@Bean(name = "systemSqlSessionFactory")@Primarypublic SqlSessionFactory systemSqlSessionFactory(@Qualifier("systemDataSource") DataSource dataSource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setMapperLocations(new PathMatchingResourcePatternResolver()//配置xml文件位置.getResources(mapperLocations));//配置实体类位置bean.setTypeAliasesPackage(typeAliasesPackage);//开启驼峰命名Objects.requireNonNull(bean.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true);return bean.getObject();}@Bean(name = "systemTransactionManager")@Primarypublic DataSourceTransactionManager systemTransactionManager(@Qualifier("systemDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Bean(name = "systemSqlSessionTemplate")@Primarypublic SqlSessionTemplate systemSqlSessionTemplate(@Qualifier("systemSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {return new SqlSessionTemplate(sqlSessionFactory);}}

competition:

@Configuration@MapperScan(basePackages = "com.yxmy.mapper.competition", sqlSessionFactoryRef = "competitionSqlSessionFactory")public class CompetitionDataSourceConfig {@Value("${spring.datasource.yxmy-competition.url}")private String url;@Value("${spring.datasource.yxmy-competition.username}")private String username;@Value("${spring.datasource.yxmy-competition.password}")private String password;@Value("${spring.datasource.yxmy-competition.mapper-locations}")private String mapperLocations;@Value("${spring.datasource.yxmy-competition.driver-class-name}")private String driverClassName;@Value("${spring.datasource.yxmy-competition.type-aliases-package}")private String typeAliasesPackage;@Primary@Bean(name = "competitionDataSource")public DruidDataSource setDataSource() {DruidDataSource dataSource = new DruidDataSource();dataSource.setUrl(url);dataSource.setUsername(username);dataSource.setPassword(password);dataSource.setDriverClassName(driverClassName);return dataSource;}@Primary@Bean(name = "competitionSqlSessionFactory")public SqlSessionFactory competitionSqlSessionFactory(@Qualifier("competitionDataSource") DataSource dataSource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setMapperLocations(new PathMatchingResourcePatternResolver()//配置xml文件位置.getResources(mapperLocations));//配置实体类位置bean.setTypeAliasesPackage(typeAliasesPackage);//开启驼峰命名Objects.requireNonNull(bean.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true);return bean.getObject();}@Primary@Bean(name = "competitionTransactionManager")public DataSourceTransactionManager competitionTransactionManager(@Qualifier("competitionDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Primary@Bean(name = "competitionSqlSessionTemplate")public SqlSessionTemplate competitionSqlSessionTemplate(@Qualifier("competitionSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {return new SqlSessionTemplate(sqlSessionFactory);}

项目使用Druid连接池,Spring同时也支持其他连接池,详细信息可以了解Spring中的DataSource的配置。同时配置中还包含对事务管理器的配置DataSourceTransactionManager,以及spring-mybatis的核心SqlSessionTemplate。

启动项目如果没有报错,就可以开始测试。

四、测试连接

最初测试写在测试类中,并且开启事务管理,在插入数据的时候没有任何报错,但是会发生回滚,当初一直是以为事务管理器配的有问题,或者事务开启方式有问题,后来发现是写在测试类才会出现,具体原因没有过多去了解,建议大家测试就写在业务代码中,去调用接口测试。

在JUnit4的测试方法中打事务注解@Transactional,默认会按照@Rollback(true)来进行处理,无论如何都会回滚,打不打注解@Rollback或@Rollback(true)已经不重要了。而在@Transactional的基础上加上@Rollback(false)之后,效果就好像是单单这个测试函数这一层没有打事务似的,而不会传播到嵌套的service层的事务内,即如果在service层的事务中,插入数据后又发生异常,最终在service层里还是会进行rollback,数据并不会插入到数据库中。

原文链接如下:JUnit之事务回滚_rolled back transaction for test-CSDN博客

4.1 前期准备

需要在两个数据库中新建两个数据表用于测试,并且设置唯一约束用于后面测试事务一致性。

sql语句如下:

create table test_table2(idint auto_incrementprimary key,namevarchar(20) null,age int not null,phone varchar(11) null,constraint test_table1_name_uindexunique (name),constraint test_table1_phone_uindexunique (phone));

编写插入语句:

insert into test_table2 (name, age, phone) values (#{name},#{age},#{phone})

新建实体类:

@Data@Builderpublic class TestEntity {private String name;private Long age;private String phone;}

编写实现类方法用于测试:(控制层接口随便写个就行)

4.2 开始测试

现在我想让这两个插入操作在同一个事务中,那么在单数据源的情况下,我们在方法上加上一个@Transactional注解开启事务(别忘了在启动类上使用@EnableTransactionManagement 开启事务管理),那么对于多数据源是否有用? 答案是

启动项目并访问调用接口,可以看到如下信息:

可以看到项目中我们配置了两个事务管理器,但是Spring不知道该使用哪一个,所以需要我们指定事务管理器。

那么对于两个数据源,我们是否可以将他们加入到同一个事务管理器中,经过实践是可以的,关于Spring的事务管理器,大家可以看看开头给出链接的文章。这里采用最为简单的使用注解的方式,文章中也介绍了该如何自定义事务,方便后期更具不同的业务进行灵活调整,同时推荐去了解一下关于Spring中的事务传播机制。

关于@Transational注解:

  • @Transactional 注解只有作用到 public 方法上事务才生效

  • 不推荐在接口上使用 @Transactional 注解

    原因:在接口上使用注解,只有在使用基于接口的代理(JDK)时才会生效,因为注解是不能继承的,这就意味着如果正在使用基于类的代理(CGLIB)时,那么事务的设置将不能被基于类的代理所识别

  • 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败

  • 默认情况下,事务只有遇到运行期异常 和 Error 会导致事务回滚,但是在遇到检查型(Checked)异常时不会回滚

    • 继承自 RuntimeException 或 error 的是非检查型异常,比如空指针和索引越界,而继承自 Exception 的则是检查型异常,比如 IOException、ClassNotFoundException,RuntimeException 本身继承 Exception

    • 非检查型类异常可以不用捕获,而检查型异常则必须用 try 语句块把异常交给上级方法,这样事务才能有效

更多信息可以看看其他文章,在此不再展开。

4.3 事务

此时,我们所需要做的就是在@Transational注解中指明事务管理器,但是现在目前项目中存在两个事务管理器,应该如何选择事务管理器是关键。

由前面的配置我们可以知道,在配置DataSourceTransactionManager时,我们在其中填入了DataSource参数指定了数据源。所以事务管理器只对其对应的数据源的操作才会有用。

4.3.1 错误示范

当前操作数据源的事务管理器本应该是competitionTransactionManager,但是此时我选择使用systemTransactionManager,并且在其中制造一个除数为0的异常,看是否会发生回滚。执行方法后情况如下:

控制台正常报错,但是数据库中数据插入,并没有发生我们想要的回滚:

4.3.2 正确示范

使用对应数据源的事务管理器,我将systemTransactionManager修改为competitionTransactionManager,并且将数据库中的数据删除,重启项目并重新调用方法。

控制台正常报错,但是数据库中没有新数据插入:

我们将 int i = 1/0; 去掉,再试一次。

控制台无报错, 数据库中存在数据,并且id为3,说明在上次操作过程中发生了回滚:

五、 结语

至此多数据源的配置与使用就暂时结束,目前虽然配置了多数据源,但是事务的实现仍是对于但数据源,那么对于在一个方法中应该如何去使用多数据源操作,同时保证事务一致性,即跨库事务(分布式事务)将在后面更新(第一版的文章写的问题,深感抱歉,现已更正)。

欢迎大家指出文章的不足之处,我一定认证修改,再次感谢各位优秀创作者所写的优秀文章。