Springboot 2.x + JPA 配置多数据源实战

当进行web开发的时候,有时候会出现需要调用另一个数据库的需求。这篇文章将教会你如何达到此目的,并提供了一键即用的完整代码给各位尝试。

案例

假设一个场景:你现在担任后端应用研发岗位,并且公司拥有一个很棒的中台体系,其中有A B两个微服务(例如A是淘宝网平台,B是支付宝平台),由于A平台是先研发并没有考虑到之后的新产品会共用user信息,因此user表存储在A服务对应的数据库中。而B服务同样也有自己的数据库,那么如何让B能拿到A的user表中的信息来达到user互通呢?

首先我要说明的是,如果你立即想到了最佳实践方案,说明你已经具备了一些优秀的微服务研发思路,恭喜你;倘若你将继续向下看,我相信你一定会在思维的碰撞与相互印证中有新的收获。

先来思考解决方案:

  • A和B直接共用一个库? 这是最不合适的方式,因为AB本属于两个产品,合库代表着更高的耦合度以及更不清晰的边界分割。

  • A和B中抽出一个C服务,并将user表迁移至C,并提供API供A和B使用? 看起来这是一个挺不错的方案,并且很多思想都会有类似的方向引导。我承认,这个解决方案对了一大半:将user服务抽象为独立的service是不错的理念,毕竟user本身就是一个能够进行很大程度抽象的模块。但在提供API这一点上有传输性能、网络延迟,依赖的稳定性等诸多问题。

第二种解决方案是合适的,但需要去优化它,这就是今天要讨论的内容:在同一个项目中配置多数据源。作为开发B项目的工程师的我们,先将C项目的依赖导入进来,那么C项目中仍然在用B的数据源,怎么办?不要怕,赶紧瞧一瞧Springboot 2.x + JPA +Hibernate的多数据源配置方案吧!

实现

首先请允许我安利一波我上传到git的demo项目,它能够经过几个非常简单的改动后直接启动运行(只给代码但是跑不起来这种事我是坚决不能做的)。下面我会对照着代码与大家一起交流多数据源的配置流程。如果你觉得不错,请给我的git项目Star一下,谢谢大家。

Git项目地址: https://github.com/Mr-zyx/multi-db-demo

现在咱们去git上下载好项目,然后跟着我一起学习吧!

Step1 - 运行项目

  • 首先需要找一个空的文件夹运行git命令下载demo项目
git clone https://github.com/Mr-zyx/multi-db-demo.git
  • 将下载好的Springboot项目放在Intellij idea中以maven项目的形式import进来。
  • 找到src/main/resources/mysql/init.sql 文件,在你的本地数据库中运行所有的SQL命令,此时数据库结构、表以及表内数据就准备好了。

这个地方我额外说一下,提供的sql query生成了demo_primary和demo_secondary两个库,第一个库中建了books,users,schools 共3个表,并为其填入了一些数据;第二个库中只建立了books,users 共两个表,并填充了不同的数据。

  • 找到src/main/java/com/example/MultiDbDemoApplication.java文件,直接启动main方法将项目启起来。

Step2 - 跟着项目开始看多数据源的配置

1. 配置多数据源

先看src/main/resources/application.properties文件,配置如下:

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/demo_primary?useSSL=false&useUnicode=yes&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
spring.datasource.primary.username=root
spring.datasource.primary.password=123456

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/demo_secondary?useSSL=false&useUnicode=yes&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
spring.datasource.secondary.username=root
spring.datasource.secondary.password=123456

对比一些正常的单数据源配置:

spring.datasource.url=jdbc:mysql://localhost:3306/demo_primary?useSSL=false&useUnicode=yes&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456

区别显而易见:

  1. 当配置了多个数据源时,前缀略有不同。 这是为了在引入时方便以前缀作为区分。
  2. spring.datasource.url 变成了spring.datasource.xxx.jdbc-url 可以看到配置url的后缀变了,这一点是springboot2的配置要求。

2. 构建DataSource的bean

@Configuration
public class DataSourceConfig {

    /***
     * 配置主数据源
     * @return
     */
    @Bean(name = "primaryDataSource")
    @Qualifier("primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    /****
     * 配置2号数据源
     * @return
     */
    @Bean(name = "secondaryDataSource")
    @Qualifier("secondaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

注册了两个Bean,目的是使用@ConfigurationProperties注解寻找到对应前缀的DB相关系统配置。

3. 配置数据源对应的映射

两个不同的源已被载入程序中,但如何知道哪个repo(dao)调用的是哪个数据源的内容呢?

这是主数据源的配置代码:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactoryPrimary",
        transactionManagerRef="transactionManagerPrimary",
        basePackages= { "com.example.repository.**" }) //设置Repository所在位置
public class RepositoryPrimaryConfig {
    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;
    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    @Primary
    @Bean(name = "entityManagerFactoryPrimary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(
            EntityManagerFactoryBuilder builder) {
        //网上文章大多数都是jpaProperties.getHibernateProperties(dataSource);就直接得到了hibernate的配置map,
        //但这个方法在springboot2.0+被舍弃了,所以这里改成这样。
        Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());
        return builder.dataSource(primaryDataSource).properties(properties)
                .packages("com.example.model.**").build();//实体包路径
    }

    @Primary
    @Bean(name = "transactionManagerPrimary")
    public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
    }
}

2号数据源代码:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactorySecondary",
        transactionManagerRef = "transactionManagerSecondary",
        basePackages = {"com.example.repository.secondary"}
        ) //设置Repository所在位置
public class RepositorySecondaryConfig {
    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;
    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    @Bean(name = "entityManagerFactorySecondary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary(
            EntityManagerFactoryBuilder builder) {
        //网上文章大多数都是jpaProperties.getHibernateProperties(dataSource);就直接得到了hibernate的配置map,
        //但这个方法在springboot2.0+被舍弃了,所以这里改成这样。
        Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());
        return builder.dataSource(secondaryDataSource).properties(properties)
                .packages("com.example.model.secondary").build();//实体的包路径
    }

    @Bean(name = "transactionManagerSecondary")
    public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject());
    }
}

开始分析这两段代码,可以看出它们的相似度很高,从不相似的地方开始看:

  1. 主数据源类的各种方法都有@Primary修饰,说明没有明确定义的时候默认使用主类。
  2. @EnableJpaRepositoriesdataSource.packages("com.example.model.secondary")里的路径问题。
    这一点我需要着重强调。可以从注释中看到它们分别对应数据库持久层和model层的路径,并且使用ant风格的配置(可以使用*号和**号来表示下方层级)。
    ⚠️路径只用写到文件夹层级即可,不要错误的配置到文件级别。
    ⚠️路径支持配置多个,在括号内填多个即可。 e.g. ("aa.bb.cc", "mm.nn.*")
    ⚠️切记所有路径加起来要覆盖所有持久层的文件夹,否则会报错。但可以用一个取巧的办法:利用ant风格优先匹配更精确的路径的原则,将主数据源直接配置为一个能覆盖全局的路径,这样只需要单独配置哪些repo/model文件夹属于副数据源即可。当使用此方案时,务必注意在系统配置文件(以properties文件为例)中加入允许bean的定义覆写的配置spring.main.allow-bean-definition-overriding=true

4. 测试

我提供了几个测试接口供大家使用,以确认配置的正确性:
GET localhost:11000/api/users/primary
GET localhost:11000/api/users/secondary
GET localhost:11000/api/books
GET localhost:11000/api/schools

实践出真知,运行一下看看是不是得到你想要的结果吧!

你可能感兴趣的:(Java)