SpringBoot2项目中(JPA + Druid)使用多数据源
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之前把所有该学的东西都学完...还有三年,时间不多了。
...说起来——最近好像新法律公布蹲幼儿园要介入刑事责任了???