SpringBoot項目中JPA使用多數據源(舉例用Database和Druid兩種配置方式 注:我僅寫Druid的基礎數據庫配置)
注:代碼部分因為影響閱讀我將它們折疊起來惹,注意前面有小箭頭的文本嗷
本文代碼篇幅較長,我願意寫,你願意聽看嘛?
技術棧(僅說一些必要的,記着要對症下葯,避免因為環境不對而不能使用)
- mysql-connector-java 8.0.22 // 說真的有了druid之后我有段懷疑有沒有必要要這個東西了... 等看明白了druid之后我回來再更新一下。
- druid-spring-boot-starter 1.2.3
- spring-boot 2.4
- spring-boot-starter-data-jpa
P.S.
- 需要的jar包可以直接在MavenRepository里搜索下載
- 這里的配置是基於注冊中心的
- 業務背景只是些雜談,具體實現直接跳轉到
實現過程
中
預先說明
本項目內容我是使用Kotlin編寫的,如果你用的IDE是IJ的產品,那么可以直接復制到Java代碼中,IDE會自動編譯成Java代碼,但是不能保證所有的代碼都是正確的,所以需要自己手動修復一部分,放心,不會太多的。
喵的用了三天時間才完成這個模塊...kotlin害龍不淺吶(小聲bb)
對於這部分代碼我計划加上其它功能后封裝一下,作為一個模版項目開源,不過短時間里並沒有足夠的時間去做它。
業務背景
在寫了對方三個管理系統之后,展開了一次新的關於數據整合的業務,在這個業務中,我們需要拿到多個項目后台的數據集。 在這里我想到了兩種解決方案,分別介紹一下其優劣。
以下內容我將跑業務的服務器統稱為業務后台
,將整合數據使用的后台稱為數據后台
- 通過不同業務中的后台中提供相對應的api來獲取所需要的數據
- ★ 可以更快地實現(添加接口)而無需重新配置一個項目(懶人專用)
- ★ 對於數據后台來說能夠更好的管理接口(通用的東西很多,可以很好地實現模塊化)
- ☆ 權限的對接要單獨寫一個模塊
- ☆ 如果圖表有更變的話,需要修改所對應的業務,這樣會讓項目變得很亂
- ☆ 除了查詢的網絡請求延時之外,中間還會再加一段網絡數據請求(幾乎可以無視,除非——)
- 一個后台進行多個數據庫的鏈接,自己拉取得所需要的數據
- ★ 修改時不容易影響到其它的業務(獨立服務)
- ★ 減少中間的數據請求過程,讓工廠與賣家少一層代理(你們都知道代理是要賺錢的吧?)
- ★ 數據整合統一在一個地方,易於處理,方便中間的數據測試(不需要再改大量的配置文件,不過我確定現在有辦法解決這個問題,貌似阿里的學習套件里就包含了test和prod的運行環境部署,或者是部署為docker鏡像,不過我還沒嘗試過,暫時)
既然是做了數據的整合,對於多數據庫的訪問就是必不可少的了,接下來的就是這篇文章的正題。
實現過程
SpringBoot配置數據庫有兩個階段(2、3):
- 配置文件中加入數據信息(注冊中心的方式配置)
- DataSourceConfig(入口 注入一些基本信息,類似於對象生成)
- 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之前把所有該學的東西都學完...還有三年,時間不多了。
...說起來——最近好像新法律公布蹲幼兒園要介入刑事責任了???