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=update
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()!!)
    }
}
M 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=update
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=update
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 😛

一些可以的改進 時間不允許,所以先將想法記錄

對於目前的jpa多源,很多東西都是相似的,完全可以對這些代碼再次抽象一下,做成一個多源數據庫的動態配置器。


結尾,希望這篇文章能夠讓所有人代碼一次跑成。如果因為這篇博客某個地方行不通的話,務必和我聯系,並說明報錯部分,我會在最短時間里回復並更正(正常狀況下24小時內可以回復)

因為寫博客的時候是十一點半,現在已經是早上六點,四點鍾的樣子感覺整條龍都飄了一下下...總之,如果有問題務必聯系(我不想丟一篇錯誤的博客誤導人,目前為止我自己測試是沒問題)

這兩天項目整合到另一個服務器,然后會再寫一篇Java和Mybatis的多數據庫配置,我想那個人用的會多一些的吧...

一些未來的目標 - 2021-01-01

其實寫完這篇文章的時候已經是2號了,整個博客期間我將代碼封裝了一次,然后調試,測試就用了差不多五個小時才算是得到這樣的結果。當時這個項目項目在配置數據庫的時候就用了一天半的時間,很多東西都是新接觸的,並不能理解,只是在自己配置完了回頭看的時候才是清晰的。
然后這次的項目自己嘗試用了一次kotlin,寫得十分費力。主要問題還是在不能new對象。。。真的是,把我們我這種單身人..啊不,龍士最享受的事情剝削了。 SpringBoot我倒是沒有系統學過,完全憑對其機制的理解瞎摸,基本上試兩次就能成。
下一個項目計划開始用RPC開始敲了,一點點進步吧,很快后台的所有類型的業務都要摸完一遍了,算法和人工智能的學習也不能怠惰,還有那些罄竹難書未實現的瘋狂的夢想。

爭取23之前把所有該學的東西都學完...還有三年,時間不多了。

...說起來——最近好像新法律公布蹲幼兒園要介入刑事責任了???


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM