SpringBoot2项目中(JPA + Druid)使用多数据源

SpringBoot项目中JPA使用多数据源(举例用Database和Druid两种配置方式 注:我仅写Druid的基础数据库配置)

注:代码部分因为影响阅读我将它们折叠起来惹,注意前面有小箭头的文本嗷

本文代码篇幅较长,我愿意写,你愿意听看嘛?

技术栈(仅说一些必要的,记着要对症下药,避免因为环境不对而不能使用)

  1. mysql-connector-java 8.0.22 // 说真的有了druid之后我有段怀疑有没有必要要这个东西了... 等看明白了druid之后我回来再更新以下。
  2. druid-spring-boot-starter 1.2.3
  3. spring-boot 2.4
  4. spring-boot-starter-data-jpa

P.S.

需要的jar包可以直接在MavenRepository里搜索下载

这里的配置是基于注册中心的

业务背景只是些杂谈,具体实现直接跳转到实现过程

预先说明

本项目内容我是使用Kotlin编写的,如果你用的IDE是IJ的产品,那么可以直接复制到Java代码中,IDE会自动编译成Java代码,但是不能保证所有的代码都是正确的,所以需要自己手动修复一部分,放心,不会太多的。

喵的用了三天时间才完成这个模块...kotlin害龙不浅呐(小声bb)

对于这部分代码我计划加上其它功能后封装一下,作为一个模版项目开源,不过短时间里并没有足够的时间去做它。

业务背景

在写了对方三个管理系统之后,展开了一次新的关于数据整合的业务,在这个业务中,我们需要拿到多个项目后台的数据集。 在这里我想到了两种解决方案,分别介绍一下其优劣。

以下内容我将跑业务的服务器统称为业务后台,将整合数据使用的后台称为数据后台

  1. 通过不同业务中的后台中提供相对应的api来获取所需要的数据
    • ★ 可以更快地实现(添加接口)而无需重新配置一个项目(懒人专用)
    • ★ 对于数据后台来说能够更好的管理接口(通用的东西很多,可以很好地实现模块化)
    • ☆ 权限的对接要单独写一个模块
    • ☆ 如果图表有更变的话,需要修改所对应的业务,这样会让项目变得很乱
    • ☆ 除了查询的网络请求延时之外,中间还会再加一段网络数据请求(几乎可以无视,除非——)
  2. 一个后台进行多个数据库的链接,自己拉取得所需要的数据
    • ★ 修改时不容易影响到其它的业务(独立服务)
    • ★ 减少中间的数据请求过程,让工厂与卖家少一层代理(你们都知道代理是要赚钱的吧?)

    • 数据整合统一在一个地方,易于处理,方便中间的数据测试(不需要再改大量的配置文件,不过我确定现在有办法解决这个问题,貌似阿里的学习套件里就包含了test和prod的运行环境部署,或者是部署为docker镜像,不过我还没尝试过,暂时)

既然是做了数据的整合,对于多数据库的访问就是必不可少的了,接下来的就是这篇文章的正题。

实现过程

SpringBoot配置数据库有两个阶段(2、3):

  1. 配置文件中加入数据信息(注册中心的方式配置)
  2. DataSourceConfig(入口 注入一些基本信息,类似于对象生成)
  3. DataBaseConfig(数据库配置 目的在于指定数据库所服务的区域)

在这里,分为两个步骤实现,第一步实现通用方法,第二步是实现分库配置的方法

P.S.

这里面两个板块的方法都是可以直接使用的(直接将通用方法或者定制方法的代码全部复制进去使用),定制化的配置相当于通用方法的添加内容,我会表明哪些是添加的内容,具体方便自己写。

虽然我比较讨厌这么做,因为太过冗余...不过我也做过一个使用者,对于我们用户来说,我们更喜欢这样的拿来即用的东西。

通用方法

先上项目结构,快速认清局势(为了生成一个树状图,专门下了个brew,各种恶心的问题...):

 origin  # 因为前面的一堆东西太长,干扰视线,所以也就没有加进去了,你们能明白就行
 ├── config
 │   ├── DataSourceConfig.kt  # 入口文件,这里用了Druid
 │   ├── ServiceAConfig.kt  # 业务A使用的数据库配置
 │   └── ServiceBConfig.kt  # 业务B使用的数据库配置
 ├── serviceA
 │   └── dao
 │       └── ServiceADao.kt  # 这是个Dao,不用我解释了吧?
 └── serviceB
     └── dao
         └── ServiceBDao.kt  # 我记得他们写JPA的喜欢命名为Repository??
DataSourceConfig.kt
// DataSourceConfig.kt

@Configuration
class DataSourceConfig {
    @Primary  //  默认数据库要加Primary关键词修饰
    @Bean("serviceADataSource")  // Bean名称,还是起一下的好
    @Qualifier("serviceADataSource")  // 数据源的分类标记(就像公狗在树下撒尿)

    // yml or properties下的配置内容,将内容通过控制中心直接注入
    @ConfigurationProperties(prefix = "spring.datasource.serviceA")
    fun serviceADataSource(): DataSource {
        return DruidDataSource()
        // 这里,如果用原生的数据库的话,用下面注释掉的内容即可(我在下面的properties配置中仅配置了Druid的写法,原生的需要你自己去改写)
//      return DataSourceBuilder.create().build()
    }

    @Bean("serviceBDataSource")
    @Qualifier("serviceBDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.serviceB")
    fun serviceBDataSource(): DataSource {
        return DruidDataSource()
    }
}
ServiceAConfig.kt
// ServiceAConfig.kt

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "serviceAEntityManagerFactory",
        transactionManagerRef = "serviceATransactionManager",
        basePackages = ["com.arunoido.origin.serviceA"]  // 这里是数据库指向的包名,我这里用的是我自己的包名。愿意的话你可以具体指向到自己的Dao层([com.arunoido.origin.serviceA.dao])
)
class ServiceAConfig {

    @Autowired
    @Qualifier("serviceADataSource")
    private lateinit var dataSource: DataSource

    @Primary
    @Bean(name = ["serviceAEntityManager"])
    fun entityManager(builder: EntityManagerFactoryBuilder): EntityManager? {
        return entityManagerFactory(builder).getObject()?.createEntityManager()
    }

    @Primary
    @Bean(name = ["serviceAEntityManagerFactory"])
    fun entityManagerFactory(builder: EntityManagerFactoryBuilder): LocalContainerEntityManagerFactoryBean {

        return builder
                .dataSource(dataSource)
                .packages("com.arunoido.origin.serviceA.model")  // 设置实体类所在位置
                .build()
    }

    @Primary
    @Bean(name = ["serviceATransactionManager"])
    fun transactionManager(builder: EntityManagerFactoryBuilder): PlatformTransactionManager? {
        return JpaTransactionManager(entityManagerFactory(builder).getObject()!!)
    }
}
ServiceBConfig.kt
// ServiceBConfig.kt

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "serviceBEntityManagerFactory",
        transactionManagerRef = "serviceBTransactionManager",
        basePackages = ["com.arunoido.origin.serviceB"]  // 可以指向多个包名,你懂的
)
class ServiceBConfig {

    @Autowired
    @Qualifier("serviceBDataSource")
    private lateinit var dataSource: DataSource

    @Bean(name = ["serviceBEntityManager"])
    fun entityManager(builder: EntityManagerFactoryBuilder): EntityManager? {
        return entityManagerFactory(builder).getObject()?.createEntityManager()
    }

    @Bean(name = ["serviceBEntityManagerFactory"])
    fun entityManagerFactory(builder: EntityManagerFactoryBuilder): LocalContainerEntityManagerFactoryBean {

        return builder
                .dataSource(dataSource)
                .packages("com.arunoido.origin.serviceB.model")
                .build()
    }

    @Bean(name = ["serviceBTransactionManager"])
    fun transactionManager(builder: EntityManagerFactoryBuilder): PlatformTransactionManager? {
        return JpaTransactionManager(entityManagerFactory(builder).getObject()!!)
    }
}

配置文件:

└── resources
   ├── application-dev.properties
   └── application.properties

P.S.

这里我使用的配置文件是跑在开发环境的properties,如果你习惯写yml的话可以自己改过去,关键词相同,只是结构不同了(我其实挺喜欢yml的结构的,主要是想尝试下新东西,嗯)

然后使用dev的配置是在application.properties中的spring.profiles.active=dev

application-dev.properties
# ServiceA的数据库
spring.datasource.serviceA.url=jdbc:mysql://ipaddress:port/serviceA?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.serviceA.username=username
spring.datasource.serviceA.password=pwd
spring.datasource.serviceA.driver-class-name=com.mysql.cj.jdbc.Driver
# ServiceB的数据库
spring.datasource.serviceB.url=jdbc:mysql://ipaddress:port/serviceB?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.serviceB.username=username
spring.datasource.serviceB.password=pwd
spring.datasource.serviceB.driver-class-name=com.mysql.cj.jdbc.Driver
# 通用的JPA配置
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# Druid的配置,如果不用Druid的话自己配置一下
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=10
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=10
#配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
#打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=1000
spring.datasource.druid.filter.stat.merge-sql=false
spring.datasource.druid.filter.wall.config.multi-statement-allow=true

通用方法覆写(定制化配置)

说明:

一般情况下,我们需要用两种jpa的策略的时候才会用到这里的内容,否则上面的默认配置完全足够使用。

我举一个最简单的可以用到这种方式配置的场景——业务A的数据库使用的是Mysql,业务B使用的数据库是Oracle,这个时候就需要把他们的Driver分别配置了。

老规矩,先上项目结构(M -> Modify):

└── origin
   ├── config
   │   ├── DataSourceConfig.kt
 M │   ├── ServiceAConfig.kt
 M │   ├── ServiceBConfig.kt
 + │   └── VendorPropertiesConfig.kt
 + ├── global
 + │   └── JpaProperties.kt
   ├── serviceA
   │   └── dao
   │       └── ServiceADao.kt
   └── serviceB
       └── dao
           └── ServiceBDao.kt
+ VendorPropertiesConfig.kt
// VendorPropertiesConfig.kt

@Configuration
class VendorPropertiesConfig {
    /**
     *
     * @return {JpaProperties} jpaProperties
     * 这个类可以覆盖通用属性
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.jpa.properties.serviceA")  // 地址可以随意点,只要不和框架的地址冲突就好
    fun getServiceAProperties(): JpaProperties {
        return JpaProperties()  // 这里用自己写的JpaProperties类,注意不要导错包
    }
    /**
     *
     * @return {JpaProperties} jpaProperties
     * ServiceB的属性
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.jpa.properties.serviceB") 
    fun getServiceBProperties(): JpaProperties {
        return JpaProperties()  
    }
}

JpaProperties我需要说明一下,这里我只列举了几个我用到的配置项,所以只写了四个,你需要以此类推的去写自己用到的选项。

这里的格式我参考了Druid的写法。

+ JpaProperties.kt
// JpaProperties.kt

/**
 *
 * 说明一下,这就是个kotlin版的JavaBean,你只需要把它作为JavaBean写,然后加上两个内部的处理方法就好了,该写getter/setter的写getter/setter。
 * 写lombok的嘛...我不建议写lombok,本龙是亲身体验过lombok版本问题导致的项目无法运行,别问我为什么不改版本,因为EAP和Ultimate的lombok版本本来就不同步。
 */
data class JpaProperties(
        var ddl_auto: String?,
        var dialect: String?,
        var physical_naming_strategy: String?,
        var implicit_naming_strategy: String?,
) {
    constructor() : this(null, null, null, null)

    private fun setConfig(): HashMap<String, *> {
        val properties = HashMap<String, Any>()
        val prefix = "hibernate."

        if (!ddl_auto.isNullOrBlank())
            properties["${prefix}ddl-auto"] = ddl_auto!!

        if (!dialect.isNullOrBlank())
            properties["${prefix}dialect"] = dialect!!

        if (!physical_naming_strategy.isNullOrBlank())
            properties["${prefix}physical_naming_strategy"] = physical_naming_strategy!!

        if (!implicit_naming_strategy.isNullOrBlank())
            properties["${prefix}implicit_naming_strategy"] = implicit_naming_strategy!!

        return properties

    }

    fun getProperties(): HashMap<String, *> {
        return setConfig()
    }

}

DataSourceConfig.kt
// DataSourceConfig.kt

@Configuration
class DataSourceConfig {
    @Primary  //  默认数据库要加Primary关键词修饰
    @Bean("serviceADataSource")  // Bean名称,还是起一下的好
    @Qualifier("serviceADataSource")  // 数据源的分类标记(就像公狗在树下撒尿)

    // yml or properties下的配置内容,将内容通过控制中心直接注入
    @ConfigurationProperties(prefix = "spring.datasource.serviceA")
    fun serviceADataSource(): DataSource {
        return DruidDataSource()
        // 这里,如果用原生的数据库的话,用下面注释掉的内容即可(我在下面的properties配置中仅配置了Druid的写法,原生的需要你自己去改写)
//      return DataSourceBuilder.create().build()
    }

    @Bean("serviceBDataSource")
    @Qualifier("serviceBDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.serviceB")
    fun serviceBDataSource(): DataSource {
        return DruidDataSource()
    }
}
M ServiceAConfig.kt
// ServiceAConfig.kt

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "serviceAEntityManagerFactory",
        transactionManagerRef = "serviceATransactionManager",
        basePackages = ["com.arunoido.origin.serviceA"]  // 这里是数据库指向的包名,我这里用的是我自己的包名。愿意的话你可以具体指向到自己的Dao层([com.arunoido.origin.serviceA.dao])
)
class ServiceAConfig {

    /*todo Modify*/@Autowired
    /*todo Modify*/lateinit var vendorPropertiesConfig: VendorPropertiesConfig

    @Autowired
    @Qualifier("serviceADataSource")
    private lateinit var dataSource: DataSource

    @Primary
    @Bean(name = ["serviceAEntityManager"])
    fun entityManager(builder: EntityManagerFactoryBuilder): EntityManager? {
        return entityManagerFactory(builder).getObject()?.createEntityManager()
    }

    @Primary
    @Bean(name = ["serviceAEntityManagerFactory"])
    fun entityManagerFactory(builder: EntityManagerFactoryBuilder): LocalContainerEntityManagerFactoryBean {

        return builder
                .dataSource(dataSource)
                /*todo Modify*/.properties(vendorPropertiesConfig.getServiceAProperties().getProperties())
                .packages("com.arunoido.origin.serviceA.model")  // 设置实体类所在位置
                .build()
    }

    @Primary
    @Bean(name = ["serviceATransactionManager"])
    fun transactionManager(builder: EntityManagerFactoryBuilder): PlatformTransactionManager? {
        return JpaTransactionManager(entityManagerFactory(builder).getObject()!!)
    }
}
ServiceBConfig.kt
// ServiceBConfig.kt

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "serviceBEntityManagerFactory",
        transactionManagerRef = "serviceBTransactionManager",
        basePackages = ["com.arunoido.origin.serviceB"]  // 这里是数据库指向的包名,我这里用的是我自己的包名。愿意的话你可以具体指向到自己的Dao层([com.arunoido.origin.serviceB.dao])
)
class ServiceBConfig {

    /*todo Modify*/@Autowired
    /*todo Modify*/lateinit var vendorPropertiesConfig: VendorPropertiesConfig

    @Autowired
    @Qualifier("serviceBDataSource")
    private lateinit var dataSource: DataSource

    @Bean(name = ["serviceBEntityManager"])
    fun entityManager(builder: EntityManagerFactoryBuilder): EntityManager? {
        return entityManagerFactory(builder).getObject()?.createEntityManager()
    }

    @Bean(name = ["serviceBEntityManagerFactory"])
    fun entityManagerFactory(builder: EntityManagerFactoryBuilder): LocalContainerEntityManagerFactoryBean {

        return builder
                .dataSource(dataSource)
                /*todo Modify*/.properties(vendorPropertiesConfig.getServiceBProperties().getProperties())
                .packages("com.arunoido.origin.serviceB.model")  // 设置实体类所在位置
                .build()
    }

    @Bean(name = ["serviceBTransactionManager"])
    fun transactionManager(builder: EntityManagerFactoryBuilder): PlatformTransactionManager? {
        return JpaTransactionManager(entityManagerFactory(builder).getObject()!!)
    }
}

配置文件:

└── resources
 M ├── application-dev.properties
   └── application.properties

properties

JpaProperties.kt
# todo serviceA添加内容
spring.jpa.properties.serviceA.ddl_auto=create
spring.jpa.properties.serviceA.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# todo serviceB添加内容
spring.jpa.properties.serviceB.ddl_auto=update
spring.jpa.properties.serviceB.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# todo 如果需要的话,serviceCDE随你添加,前面只要按照模式添加即可

# ServiceA的数据库
spring.datasource.serviceA.url=jdbc:mysql://ipaddress:port/serviceA?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.serviceA.username=username
spring.datasource.serviceA.password=pwd
spring.datasource.serviceA.driver-class-name=com.mysql.cj.jdbc.Driver
# ServiceB的数据库
spring.datasource.serviceB.url=jdbc:mysql://ipaddress:port/serviceB?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.serviceB.username=username
spring.datasource.serviceB.password=pwd
spring.datasource.serviceB.driver-class-name=com.mysql.cj.jdbc.Driver
# 通用的JPA配置
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# Druid的配置,如果不用Druid的话自己配置一下
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=10
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=10
#配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
#打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=1000
spring.datasource.druid.filter.stat.merge-sql=false
spring.datasource.druid.filter.wall.config.multi-statement-allow=true

总结(我对总结的定义是:如果将这个内容出成一道考题的话,那么这里的内容是应该是可以直接解答问题的)

  • 项目配置文件(.properties or .yml)需要添加上两个数据库的基本信息,两个信息需要能够区分开且不能与原生的配置字段冲突
  • DataSourceConfig.kt 数据库配置的入口文件,在这里声明DataSourceBuilder()
  • ServiceA,B,C,D进行多个数据源的分发,将数据源分发到对应需要的包下
  • 如果通用的配置无法满足,可以用新的配置覆盖掉某个源的配置,需要用到VendorPropertiesConfig.kt,同时准备一个JavaBean处理注册中心注入的数据
  • enjoy coding
上一篇:Luogu P4070 [SDOI2016]生成魔咒


下一篇:leecode743:网络延迟时间