一、说明
文章中写的东西不一定是完全正确的,希望看到这篇文章的同学也可以自己上手试试,如有大佬为我指正错误,万分感谢。该文章算是记录自己折腾这个东西的一个过程,给大家分享一下过程,希望可以帮到你们一点小忙,同时往后可以给自己复习一下。第一次写文章,写的不好的地方也希望大家多多包涵,接受一切指正。
感谢以下文章给予我解决思路,感谢各位大佬。
SpringBoot中mybatis多数据源配置自动转换驼峰标识没有生效_mybatis下划线转驼峰配置不生效-优快云博客
SpringBoot+MyBatis项目中同时操作多个数据库_springboot mybatis连接多个数据库-优快云博客
springboot+mybatis+mysql+yml配置多数据源_mysql datasoure yml-优快云博客
咱们从头到尾说一次 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}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: ${yxmy.datasource.username}
password: ${yxmy.datasource.password}
mapper-locations: classpath:mapper/system/**/*.xml
type-aliases-package: com.yxmy.entity
yxmy-competition:
driver-class-name: ${yxmy.datasource.driver-class-name}
url: jdbc:mysql://${yxmy.datasource.host}:${yxmy.datasource.port}/${yxmy.datasource.competition-database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: ${yxmy.datasource.username}
password: ${yxmy.datasource.password}
mapper-locations: classpath:mapper/competition/**/*.xml
type-aliases-package: com.yxmy.entity
其中yxmy-system 和 yxmy-competition 是自定义数据源的名称,可以自己取自己想要的名字。新增了mapper-locations 和 type-aliases-package 。原本在配置文件中,对mybatis进行了单独的配置,设置了实体类存放包,xml文件位置,开启驼峰命名。现在设置了多数据源,同时对xml存放的文件结构改变,所以进行了重新配置。存放xml文件的目录结构如下:

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")
@Primary
public 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")
@Primary
public DataSourceTransactionManager systemTransactionManager(@Qualifier("systemDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "systemSqlSessionTemplate")
@Primary
public 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-优快云博客
4.1 前期准备
需要在两个数据库中新建两个数据表用于测试,并且设置唯一约束用于后面测试事务一致性。
sql语句如下:
create table test_table2
(
id int auto_increment
primary key,
name varchar(20) null,
age int not null,
phone varchar(11) null,
constraint test_table1_name_uindex
unique (name),
constraint test_table1_phone_uindex
unique (phone)
);
编写插入语句:
insert into test_table2 (name, age, phone) values (#{name},#{age},#{phone})
新建实体类:
@Data
@Builder
public 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,说明在上次操作过程中发生了回滚:

五、 结语
至此多数据源的配置与使用就暂时结束,目前虽然配置了多数据源,但是事务的实现仍是对于但数据源,那么对于在一个方法中应该如何去使用多数据源操作,同时保证事务一致性,即跨库事务(分布式事务)将在后面更新(第一版的文章写的问题,深感抱歉,现已更正)。
欢迎大家指出文章的不足之处,我一定认证修改,再次感谢各位优秀创作者所写的优秀文章。
本文讲述了如何在SpringBoot项目中配置和使用多数据源,包括MyBatis的多数据源配置,YML文件的编写,以及事务管理的注意事项。作者分享了配置示例和测试过程,以确保跨库操作的事务一致性。
2603

被折叠的 条评论
为什么被折叠?



