英文:
What is the proper way to indicate which data source to inject into my DAOs in a multi-module multi-datasource project?
问题
我有一个项目分成3个模块(目前为止)- 核心(模型1),用户管理(模型2)和Web(视图和控制器)。我项目的结构(为了简化起见,只列出与问题相关的类)如下:
项目
|-- 核心
| |-- src.main.java.com.romco.example
| | |-- config.CoreDataSourceConfiguration
| | |-- persistence.daoimpl.SomeCoreDaoImpl
|-- 用户管理
| |-- src.main.kotlin.com.romco.example
| | |-- config.UserManagementConfiguration
| | |-- persistence.daoimpl.SomeUserManagementDaoImpl
|-- Web
| // 现在不重要
我的类如下(在调试之前的问题时,我不得不将一些值初始化直接移到代码中,而不是使用application.properties,正如TODO中所指出的,为了解决当前问题,请忽略它):
- CoreDataSourceConfiguration:
@Configuration
public class CoreDataSourceConfiguration {
@Bean
@Primary
public DataSourceProperties coreDataSourceProperties() {
return new DataSourceProperties();
}
//TODO values should be retrieved from application.properties
@Bean(name = "coreDataSource")
@Primary
public DataSource coreDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
dataSourceBuilder.url("...");
dataSourceBuilder.username("...");
dataSourceBuilder.password("...");
return dataSourceBuilder.build();
}
@Bean(name = "coreTransactionManager")
@Autowired
DataSourceTransactionManager coreTransactionManager(@Qualifier("coreDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
- SomeCoreDaoImpl:
@Repository
public class SomeCoreDaoImpl implements SomeCoreDao {
// 这里有一些常量
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
@Override
public void setDataSource(DataSource dataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplateHolder.get(dataSource);
}
// 数据库代码在这里 - 创建、更新等等
}
- UserManagementConfiguration:
@Configuration
open class UserManagementDataSourceConfiguration {
@Bean
open fun userManagementDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean(name = ["userManagementDataSource"])
open fun userManagementDataSource(): DataSource {
val dataSourceBuilder = DataSourceBuilder.create()
dataSourceBuilder
.driverClassName("com.mysql.cj.jdbc.Driver")
.url("...")
.username("...")
.password("...")
return dataSourceBuilder.build()
}
@Bean(name = ["userManagementTransactionManager"])
@Autowired
open fun userManagementTransactionManager(@Qualifier("userManagementDataSource") dataSource: DataSource): DataSourceTransactionManager {
return DataSourceTransactionManager(dataSource)
}
}
- SomeUserManagementDaoImpl:
@Repository
open class SomeUserManagementDaoImpl: SomeUserManagementDao{
// 这里有一些常量
private lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
@Autowired
fun setDataSource(@Qualifier("userManagementDataSource") dataSource: DataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
}
// 数据库代码在这里
}
正如你所看到的,我让它工作的方式是在SomeUserManagementDaoImpl类内部的autowired setDataSource方法中指定要使用的bean。
显然,我希望避免在每个daoImpl类中都这样做,虽然我可以考虑将其提取到一个单独的类中,但这似乎不是“Spring”的预期解决方案。
现在(显然)- 数据源是特定于模块的,最初,我甚至认为Spring会在幕后以某种方式找出它,并且不会使用@Primary数据源,而是会使用在给定模块中定义的数据源(除非该模块没有数据源,在这种情况下,我认为它会回退到@Primary数据源)。
然而,事实并非如此,我想知道是否有一种方法告诉Spring在整个模块中使用给定的数据源配置...
我已经阅读了许多类似的主题和指南,涉及多数据源项目,但实际上我从未找到答案。实际上,当我实现多数据源解决方案时,我参考的指南根本没有提到这一点(除非我错过了),例如:
https://www.baeldung.com/spring-boot-failed-to-configure-data-source
https://www.baeldung.com/spring-data-jpa-multiple-databases
当然也有可能我在做一些其他严重错误,这可能是根本原因,如果是这样,请也帮助我解决。
英文:
I have a project split into 3 modules (so far) - core (Model 1), user-management (model 2) and web (View and Controller). My project structure (simplified to only relevant classes for the sake of getting to the point) is as follows:
Project
|-- core
| |-- src.main.java.com.romco.example
| | |-- config.CoreDataSourceConfiguration
| | |-- persistence.daoimpl.SomeCoreDaoImpl
|-- user-management
| |-- src.main.kotlin.com.romco.example
| | |-- config.UserManagementConfiguration
| | |-- persistence.daoimpl.SomeUserManagementDaoImpl
|-- web
| // not important right now
My classes are as follows (while debugging my previous issue, I had to move some value initialization directly to code instead of using application.properties, as noted by the TODO, so please ignore it for the sake of the problem at hand)
- CoreDataSourceConfiguration:
@Configuration
public class CoreDataSourceConfiguration {
@Bean
@Primary
public DataSourceProperties coreDataSourceProperties() {
return new DataSourceProperties();
}
//TODO values should be retrieved from application.properties
@Bean(name = "coreDataSource")
@Primary
public DataSource coreDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
dataSourceBuilder.url("...");
dataSourceBuilder.username("...");
dataSourceBuilder.password("...");
return dataSourceBuilder.build();
}
@Bean(name = "coreTransactionManager")
@Autowired
DataSourceTransactionManager coreTransactionManager(@Qualifier("coreDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
- SomeCoreDaoImpl:
@Repository
public class SomeCoreDaoImpl implements SomeCoreDao {
// some constants here
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
@Override
public void setDataSource(DataSource dataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplateHolder.get(dataSource);
}
// DB code here - create, update, etc.
}
- UserManagementConfiguration:
@Configuration
open class UserManagementDataSourceConfiguration {
@Bean
open fun userManagementDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean(name = ["userManagementDataSource"])
open fun userManagementDataSource(): DataSource {
val dataSourceBuilder = DataSourceBuilder.create()
dataSourceBuilder
.driverClassName("com.mysql.cj.jdbc.Driver")
.url("...")
.username("...")
.password("...")
return dataSourceBuilder.build()
}
@Bean(name = ["userManagementTransactionManager"])
@Autowired
open fun userManagementTransactionManager(@Qualifier("userManagementDataSource") dataSource: DataSource): DataSourceTransactionManager {
return DataSourceTransactionManager(dataSource)
}
}
- SomeUserManagementDaoImpl:
@Repository
open class SomeUserManagementDaoImpl: SomeUserManagementDao{
// constants are here
private lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
@Autowired
fun setDataSource(@Qualifier("userManagementDataSource") dataSource: DataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
}
// DB code here
}
As you can see, the way I made it work is by specifying which bean to use in the autowired setDataSource method inside my SomeUserManagementDaoImpl class.
I would obviously prefer to avoid having to do this in every daoImpl class, and while I can think of extracting this to a single class, it doesn't seem like that's the "spring" intended solution.
Now (again, obviously) - The data sources are module-specific, and initially, I even assumed spring would somehow figure it out under the hood and, instead of using the @Primary datasource, would use the one defined in a given module (unless that module had none, in which case I assumed it would fall back to the @Primary one).
However, that was not the case, and I'm left wondering if there is some way to tell spring to use a given data source configuration across that entire module...
I've been looking at many similiar threads and guides that deal with multi-datasource projects, but I actually never found the answer. In fact, the guides which I consulted when I was implementing my multi-datasource solution never mentioned this at all (unless I missed it), eg.
https://www.baeldung.com/spring-boot-failed-to-configure-data-source
https://www.baeldung.com/spring-data-jpa-multiple-databases
It is also entirely possible that I'm doing something else terribly wrong, and that is the root cause, in which case, please, also help me out.
答案1
得分: 0
所以,如果有人遇到同样的问题,这是我迄今为止解决的方法。也许将来我会想出一个更优雅的解决方案,或者更有可能发现这个解决方案存在问题,但目前看起来它是有效的(尽管我还没有进行太多的测试):
在用户管理模块(不使用 @Primary 数据源的模块)中,我创建了以下抽象类,将数据源注入(使用限定符指定数据源)提取到一个地方:
abstract class WithDataSource {
protected lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
@Autowired
fun setDataSource(@Qualifier(USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME) dataSource: DataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
}
}
然后,我的每个用户管理 DaoImpl 类都继承了这个类,因此隐式地实现了 GenericDao 接口的 setDataSource() 方法。
为了完整起见,现在用户管理模块的结构如下(我包含了一些以前省略的接口,但仍然保留了 "example" 的命名,并省略了一些特定的实用程序代码):
Project
|-- core
| |-- src.main.java.com.romco.example
| | |-- config.CoreDataSourceConfiguration
| | |-- persistence.daoimpl.SomeCoreDaoImpl
|-- user-management
| |-- src.main.kotlin.com.romco.example
| | |-- config.UserManagementConfiguration
| | |-- persistence.dao.GenericDao
| | |-- persistence.daoimpl.SomeUserManagementDaoImpl
| | |-- persistence.util.DaoUtil.kt
|-- web
| // 暂不重要
- UserManagementConfiguration(将 bean 名称添加为常量 USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME):
@Configuration
open class UserManagementDataSourceConfiguration {
companion object {
const val USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME = "userManagementDataSource"
}
@Bean
open fun userManagementDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean(name = ["userManagementDataSource"])
open fun userManagementDataSource(): DataSource {
val dataSourceBuilder = DataSourceBuilder.create()
dataSourceBuilder
.driverClassName("com.mysql.cj.jdbc.Driver")
.url("...")
.username("...")
.password("...")
return dataSourceBuilder.build()
}
@Bean(name = ["userManagementTransactionManager"])
@Autowired
open fun userManagementTransactionManager(@Qualifier("userManagementDataSource") dataSource: DataSource): DataSourceTransactionManager {
return DataSourceTransactionManager(dataSource)
}
}
- GenericDao(在原始问题中没有提到,因为它与解决方案的完整性不太相关):
interface GenericDao<T> {
fun setDataSource(dataSource: DataSource)
// 检索所有记录
fun retrieveAll(): Collection<T>
// 创建记录并返回新创建记录的 ID。如果失败,返回 -1。
fun create(t: T): Long
// 根据 ID 更新记录,成功则返回 true。
fun update(t: T): Boolean
// 根据 ID 删除记录,成功则返回 true。
fun delete(t: T): Boolean
// 执行清理操作,例如可能删除所有测试记录(id < 0)
fun cleanup()
}
- SomeUserManagementDao(在原始问题中没有提到,因为它与解决方案的完整性不太相关):
interface SomeUserManagementDao : GenericDao<SomeUserManagementClass> {
fun retrieveBySpecificValue(specificValue: String): SomeUserManagementClass?
}
- SomeUserManagementDaoImpl(根据注释中的提及进行了更新):
@Repository
open class SomeUserManagementDaoImpl : SomeUserManagementDao, WithDataSource() {
// 常量在此
// namedParameterJdbcTemplate 和 setDataSource() 现在从父类 WithDataSource 中继承
// 数据库代码在此
}
- DaoUtil.kt(包含最初提到的抽象类以及其他一些被省略的实用程序):
abstract class WithDataSource {
protected lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
@Autowired
fun setDataSource(@Qualifier(USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME) dataSource: DataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
}
}
英文:
So, in case anyone stumbles upon the same problem, here's how I solved it so far. I might come up with a more elegant solution in the future, or more likely, find an issue with this one, but for now it seems to be working (haven't done much testing yet though):
In the user-management module (the one NOT using the @Primary datasource), I created the following abstract class, extracting the dataSource injection (with the qualifier specifying the datasource) to one place:
abstract class WithDataSource {
protected lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
@Autowired
fun setDataSource(@Qualifier(USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME) dataSource: DataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
}
}
Each of my user-management DaoImpl classes then extends this class, and therefore implicitly implements the setDataSource() method of my GenericDao interface.
For completeness, the user-management module now looks like this (I included some previously omitted interfaces, but still left the "example" naming and omitted some specific utility code):
Project
|-- core
| |-- src.main.java.com.romco.example
| | |-- config.CoreDataSourceConfiguration
| | |-- persistence.daoimpl.SomeCoreDaoImpl
|-- user-management
| |-- src.main.kotlin.com.romco.example
| | |-- config.UserManagementConfiguration
| | |-- persistence.dao.GenericDao
| | |-- persistence.daoimpl.SomeUserManagementDaoImpl
| | |-- persistence.util.DaoUtil.kt
|-- web
| // not important right now
- UserManagementConfiguration (added bean name as constant USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME):
@Configuration
open class UserManagementDataSourceConfiguration {
companion object {
const val USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME = "userManagementDataSource"
}
@Bean
open fun userManagementDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean(name = ["userManagementDataSource"])
open fun userManagementDataSource(): DataSource {
val dataSourceBuilder = DataSourceBuilder.create()
dataSourceBuilder
.driverClassName("com.mysql.cj.jdbc.Driver")
.url("...")
.username("...")
.password("...")
return dataSourceBuilder.build()
}
@Bean(name = ["userManagementTransactionManager"])
@Autowired
open fun userManagementTransactionManager(@Qualifier("userManagementDataSource") dataSource: DataSource): DataSourceTransactionManager {
return DataSourceTransactionManager(dataSource)
}
}
- GenericDao (wasn't mentioned in original quesiton, as it was not too relevant, including for completeness of solution):
interface GenericDao<T> {
fun setDataSource(dataSource: DataSource)
// retrieves all
fun retrieveAll(): Collection<T>
// creates and returns id of the newly created record. In case of failure, returns -1.
fun create(t: T): Long
// updates by id, returns true if success.
fun update(t: T): Boolean
// deletes by id, returns true if success.
fun delete(t: T): Boolean
// performs cleanup, for example, might delete all test records (id < 0)
fun cleanup()
}
- SomeUserManagementDao (wasn't mentioned in original quesiton, as it was not too relevant, including for completeness of solution):
interface SomeUserManagementDao: GenericDao<SomeUserManagementClass> {
fun retrieveBySpecificValue(specificValue: String): SomeUserManagementClass?
}
- SomeUserManagementDaoImpl (updated as mentioned in comment):
@Repository
open class SomeUserManagementDaoImpl: SomeUserManagementDao, WithDataSource() {
// constants are here
// namedParameterJdbcTemplate and setDataSource() are now inherited from the parent class - WithDataSource
// DB code here
}
- DaoUtil.kt (contains the originally mentioned abstract class along with some other, in this case omitted, utilities):
abstract class WithDataSource {
protected lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
@Autowired
fun setDataSource(@Qualifier(USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME) dataSource: DataSource) {
namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论