目录

Springboot Mybatis Plus解决多数据源

目标

在开发中经常会遇到一个程序需要调用多个数据库的情况,总得来说分为下面的几种情况:

  1. 一个程序会调用不同结构的两个数据库。
  2. 读写分离,两个数据结构可能一样高,但是不同的操作针对不同的数据库。
  3. 混合情况,既有不同的结构的数据库,也可能存在读写分离的情况

下面针对第一种情况,提供一个解决方案。

解决思路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数据源的配置部分给删除了。

数据源配置注意事项

  1. 多数据源配置的时候,与单数据源不同点在于spring.datasource之后多设置一个数据源名称primary和secondary来区分不同的数据源配置,这个前缀将在后续初始化数据源的时候用到。
  2. 数据源连接配置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.,那么就是这个配置项的问题。
  3. 可以看到,不论使用哪一种数据访问框架,对于数据源的配置都是一样的。

建立两个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来指定具体什么包采用什么数据源。然后再对应包里就和以前单数据源一样操作即可。

注意事项

  1. 如果用myBatis, SqlSessionFactory 部分可以使用SqlSessionFactoryBean来生成。但是如果用mybatis plus一定要用MybatisSqlSessionFactoryBean 来生成SqlSessionFactory。否则会报错 ,无法直接通过BaseMapper去调用查询。
  2. 如果要再不同的包中混合上XML进行调用。需要在SqlSessionFactory的配置中设置factoryBean.setMapperLocations(resolver.getResources(“classpath*:mapper/ehr/**Mapper.xml”));

优缺点

优点:简单,通过简单的设置。就可以满足大多数的情况。

缺点:只适合多个数据源的结构完全不一样,通过package可以分来的方式来调用,不能灵活的在一个package下面随心所欲的调用数据源。

解决思路2

总config和各自的config分开,总config用于导入配置数据源,各自config用于Service、Dao层链接数据库使用

总数据源,初始化数据源与MyBatis-Plus配置

完成多数据源的配置信息之后,就来创建个配置类来加载这些配置信息,初始化数据源,以及初始化每个数据源要用的MyBatis配置。

这里我们继续将数据源与框架配置做拆分处理:

  1. 单独建一个多数据源的配置类,比如下面这样:
 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配置上。

  1. 第一个数据源,分别创建两个数据源的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());
    }

}

注意事项

说明与注意

  1. 配置类上使用@MapperScan注解来指定当前数据源下定义的Entity和Mapper的包路径;另外需要指定sqlSessionFactory和sqlSessionTemplate,这两个具体实现在该配置类中类中初始化。
  2. 配置类的构造函数中,通过@Qualifier注解来指定具体要用哪个数据源,其名字对应在DataSourceConfiguration配置类中的数据源定义的函数名。
  3. 配置类中定义SqlSessionFactory和SqlSessionTemplate的实现,注意具体使用的数据源正确(如果使用这里的演示代码,只要第二步没问题就不需要修改)。

解决思路3

基础的配置

  1. 数据源的yml配置和上一结一样,就不在赘述了。
  2. 建立一个枚举类来标识两个数据源
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对应的方法上面设置注解,就可以很灵活的实现不同的方法调用不同的数据源。