目标
在开发中经常会遇到一个程序需要调用多个数据库的情况,总得来说分为下面的几种情况:
- 一个程序会调用不同结构的两个数据库。
- 读写分离,两个数据结构可能一样高,但是不同的操作针对不同的数据库。
- 混合情况,既有不同的结构的数据库,也可能存在读写分离的情况。
下面针对第一种情况,提供一个解决方案。
解决思路1
因为两个数据库的功能和结构不一样,所以可以根据功能和结构把DAO分为两个package。然后再mapperscan中指定不同的package对接不同的数据源,即可达到多个数据源的共存。
配置yml中的数据源设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
spring:
datasource:
emanage:
#SpringBoot 1.x
url: jdbc:mysql://127.0.0.1:3306/emanage?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC&useAffectedRows=true
#SpringBoot 2.x
# jdbc-url: jdbc:mysql://127.0.0.1:3306/emanage?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC&useAffectedRows=true
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
ehr:
#SpringBoot 1.x
url: jdbc:mysql://127.0.0.1:3306/ehr?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC&useAffectedRows=true
#SpringBoot 2.x
# jdbc-url: jdbc:mysql://127.0.0.1:3306/ehr?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC&useAffectedRows=true
username: root
password: ********
driver-class-name: com.mysql.cj.jdbc.Driver
|
为了不必要的干扰,我把druid数据源的配置部分给删除了。
数据源配置注意事项
- 多数据源配置的时候,与单数据源不同点在于spring.datasource之后多设置一个数据源名称primary和secondary来区分不同的数据源配置,这个前缀将在后续初始化数据源的时候用到。
- 数据源连接配置2.x和1.x的配置项是有区别的:2.x使用spring.datasource.secondary.jdbc-url,而1.x版本使用spring.datasource.secondary.url。如果你在配置的时候发生了这个报错java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.,那么就是这个配置项的问题。
- 可以看到,不论使用哪一种数据访问框架,对于数据源的配置都是一样的。
建立两个datasource的配置
datasource1的 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Configuration
@MapperScan(basePackages = {"com.emanage.ehr.mapper.emanage"},sqlSessionTemplateRef = "sqlTemplate1")
public class DataSourceConfig1 {
@Bean(name = "datasource1")
@ConfigurationProperties(prefix = "spring.datasource.emanage")
public DruidDataSource druidDataSource1()
{
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "sqlFactory1")
public SqlSessionFactory sqlSessionFactory(@Qualifier("datasource1") DruidDataSource dataSource)
throws Exception
{
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/emanage/**Mapper.xml"));
return factoryBean.getObject();
}
@Bean(name = "sqlTemplate1")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlFactory1") SqlSessionFactory sqlSessionFactory)
{
return new SqlSessionTemplate(sqlSessionFactory);
}
}
|
datasource2的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Configuration
@MapperScan(basePackages = {"com.emanage.ehr.mapper.ehr"},sqlSessionTemplateRef = "sqlTemplate2")
public class DataSourceConfig2 {
@Bean(name = "datasource2")
@ConfigurationProperties(prefix = "spring.datasource.ehr")
public DataSource druidDataSource1()
{
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "sqlFactory2")
public SqlSessionFactory sqlSessionFactory(@Qualifier("datasource2") DataSource dataSource)
throws Exception
{
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/ehr/**Mapper.xml"));
return factoryBean.getObject();
}
@Bean(name = "sqlTemplate2")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlFactory2") SqlSessionFactory sqlSessionFactory)
{
return new SqlSessionTemplate(sqlSessionFactory);
}
}
|
两个datasource的配置基本上一样。就是建立datasource,sqlsessionFactory,sqlSessionTemplate的注入。然后通过mapperscan来指定具体什么包采用什么数据源。然后再对应包里就和以前单数据源一样操作即可。
注意事项
- 如果用myBatis, SqlSessionFactory 部分可以使用SqlSessionFactoryBean来生成。但是如果用mybatis plus一定要用MybatisSqlSessionFactoryBean 来生成SqlSessionFactory。否则会报错 ,无法直接通过BaseMapper去调用查询。
- 如果要再不同的包中混合上XML进行调用。需要在SqlSessionFactory的配置中设置factoryBean.setMapperLocations(resolver.getResources(“classpath*:mapper/ehr/**Mapper.xml”));
优缺点
优点:简单,通过简单的设置。就可以满足大多数的情况。
缺点:只适合多个数据源的结构完全不一样,通过package可以分来的方式来调用,不能灵活的在一个package下面随心所欲的调用数据源。
解决思路2
总config和各自的config分开,总config用于导入配置数据源,各自config用于Service、Dao层链接数据库使用
总数据源,初始化数据源与MyBatis-Plus配置
完成多数据源的配置信息之后,就来创建个配置类来加载这些配置信息,初始化数据源,以及初始化每个数据源要用的MyBatis配置。
这里我们继续将数据源与框架配置做拆分处理:
- 单独建一个多数据源的配置类,比如下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Configuration
public class DataSourceConfiguration {
@Primary
@Bean
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
|
通过@Configuration
可以知道这两个数据源分别加载了spring.datasource.primary.*
和spring.datasource.secondary.*
的配置。@Primary
注解指定了主数据源,就是当我们不特别指定哪个数据源的时候,就会使用这个Bean真正差异部分在下面的JPA配置上。
- 第一个数据源,分别创建两个数据源的MyBatis配置。
Primary数据源的JPA配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@Configuration
@MapperScan(
//这里如果单数据源只需写到mysql,会自动寻找mapper,多数据源要写到mapper文件夹否则报错
basePackages = "cn.edu.hubu.lhy.multiplydatabase.mysql.mapper",
sqlSessionFactoryRef = "sqlSessionFactoryPrimary",
sqlSessionTemplateRef = "sqlSessionTemplatePrimary")
public class PrimaryConfig {
private DataSource primaryDataSource;
public PrimaryConfig(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
this.primaryDataSource = primaryDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactoryPrimary() throws Exception {
// SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(primaryDataSource);
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplatePrimary() throws Exception {
return new SqlSessionTemplate(sqlSessionFactoryPrimary());
}
}
|
第二个数据源,Secondary数据源的JPA配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Configuration
@MapperScan(
//这里如果单数据源只需写到postgresql,会自动寻找mapper,多数据源要写到mapper文件夹否则报错
basePackages = "cn.edu.hubu.lhy.multiplydatabase.postgresql.mapper",
sqlSessionFactoryRef = "sqlSessionFactorySecondary",
sqlSessionTemplateRef = "sqlSessionTemplateSecondary")
public class SecondaryConfig {
private DataSource secondaryDataSource;
public SecondaryConfig(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
this.secondaryDataSource = secondaryDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactorySecondary() throws Exception {
// SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(secondaryDataSource);
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplateSecondary() throws Exception {
return new SqlSessionTemplate(sqlSessionFactorySecondary());
}
}
|
注意事项
说明与注意:
- 配置类上使用
@MapperScan
注解来指定当前数据源下定义的Entity和Mapper的包路径;另外需要指定sqlSessionFactory和sqlSessionTemplate,这两个具体实现在该配置类中类中初始化。
- 配置类的构造函数中,通过
@Qualifier
注解来指定具体要用哪个数据源,其名字对应在DataSourceConfiguration
配置类中的数据源定义的函数名。
- 配置类中定义SqlSessionFactory和SqlSessionTemplate的实现,注意具体使用的数据源正确(如果使用这里的演示代码,只要第二步没问题就不需要修改)。
解决思路3
基础的配置
- 数据源的yml配置和上一结一样,就不在赘述了。
- 建立一个枚举类来标识两个数据源
1
2
3
|
public enum DataSourceType {
emanage,ehr
}
|
建立一个线程和数据源之间的关联类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public class DataBaseContextHolder {
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(DataSourceType type)
{
if(type == null)
{
throw new NullPointerException();
}
contextHolder.set(type);
}
public static DataSourceType getDataSourceType()
{
DataSourceType type = contextHolder.get();
if(type == null)
{
//确定一个默认数据源
return DataSourceType.emanage;
}
return type;
}
public static void clearDataSrouceType()
{
contextHolder.remove();
}
}
|
代码比较简单。就是当我们设置一个Mapper是通过那个数据源去访问数据的时候,把设置的参数保存在contextHolder中,为了处理线程安全,采用ThreadLocal的方式。
定义动态数据源
1
2
3
4
5
6
7
8
|
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataBaseContextHolder.getDataSourceType();
}
}
|
定义多数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
@Configuration
@MapperScan("com.emanage.ehr.mapper")
public class DataSourceConfig {
@Autowired
private Environment env;
@Bean(name = "datasource1")
@ConfigurationProperties(prefix = "spring.datasource.emanage")
public DruidDataSource druidDataSource1()
{
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "datasource2")
@ConfigurationProperties(prefix = "spring.datasource.ehr")
public DruidDataSource druidDataSource2()
{
return DruidDataSourceBuilder.create().build();
}
@Bean
public DynamicDataSource dynamicDataSource(@Qualifier("datasource1") DruidDataSource ds1,
@Qualifier("datasource2") DruidDataSource ds2)
{
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceType.emanage, ds1);
targetDataSource.put(DataSourceType.ehr, ds2);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(ds1);
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
// 指定数据源
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(resolver.getResources("classpath*:mapper/**Mapper.xml"));
return bean.getObject();
}
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
|
使用数据源
在调用mapper
之前,在service
中执行以下代码,可以灵活的切换数据源。
1
2
|
DataBaseContextHolder.setDataSourceType(DataSourceType.emanage);
DataBaseContextHolder.setDataSourceType(DataSourceType.ehr);
|
优化升级
感觉在sevrice中调用这些代码太过繁琐,可以自己定义两个注解。
1
2
|
public @interface DataSourceEmanage{}
public @interface DataSourceEHr{}
|
然后建立一个aop类让在有些注解的mapper类执行之前,先执行相应的数据源切换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Aspect
@Component
public class DataSourceAop {
@Before("@annotation(com.example.demo3.config.DataSourceEmanage)")
public void setEmanageDataSource()
{
DataBaseContextHolder.setDataSourceType(DataSourceType.emanage);
}
@Before("@annotation(com.example.demo3.config.DataSourceEhr)")
public void setEhrDataSource()
{
DataBaseContextHolder.setDataSourceType(DataSourceType.ehr);
}
}
|
只需要在mapper对应的方法上面设置注解,就可以很灵活的实现不同的方法调用不同的数据源。