java - Spring JPA:如何在同一个请求中更新 2 个不同的“DataSource”中的 2 个不同的表?
问题描述
在我们的应用程序中,我们有一个名为的通用数据库central
,每个客户都将拥有自己的数据库,其中包含完全相同的表集。每个客户的数据库可以托管在我们自己的服务器上,也可以根据客户组织的要求托管在客户的服务器上。
为了处理这种多租户需求,我们扩展了AbstractRoutingDataSource
Spring JPA 并重写了基于传入的动态determineTargetDataSource()
创建新连接和建立新连接的方法。我们还使用一个简单的类将当前数据源上下文存储在一个变量中。我们的解决方案与本文中描述的类似。DataSource
customerCode
DatabaseContextHolder
ThreadLocal
假设在一个请求中,我们需要更新central
数据库和客户数据库中的一些数据,如下所示。
public void createNewEmployeeAccount(EmployeeData employee) {
DatabaseContextHolder.setDatabaseContext("central");
// Code to save a user account for logging in to the system in the central database
DatabaseContextHolder.setDatabaseContext(employee.getCustomerCode());
// Code to save user details like Name, Designation, etc. in the customer's database
}
determineTargetDataSource()
只有在每次执行任何 SQL 查询之前都调用此代码才能工作,以便我们可以在方法的DataSource
中途动态切换。
但是,从这个Stackoverflow question看来,当在该请求中第一次检索a 时,似乎determineTargetDataSource()
只调用一次。HttpRequest
DataSource
AbstractRoutingDataSource.determineTargetDataSource()
如果您能给我一些关于何时实际被调用的见解,我将非常感激。此外,如果您之前处理过类似的多租户场景,我很想听听您对我应该如何处理DataSource
单个请求中的多个更新的意见。
解决方案
我们找到了一个可行的解决方案,它混合了我们central
数据库的静态数据源设置和客户数据库的动态数据源设置。
本质上,我们确切地知道哪个表来自哪个数据库。因此,我们能够将我们的@Entity
类分成 2 个不同的包,如下所示。
com.ft.model
-- central
-- UserAccount.java
-- UserAccountRepo.java
-- customer
-- UserProfile.java
-- UserProfileRepo.java
随后,我们创建了两个@Configuration
类来设置每个包的数据源设置。对于我们的central
数据库,我们使用如下静态设置。
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager",
basePackages = { "com.ft.model.central" }
)
public class CentralDatabaseConfiguration {
@Primary
@Bean(name = "dataSource")
public DataSource dataSource() {
return DataSourceBuilder.create(this.getClass().getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
@Primary
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("dataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.central")
.persistenceUnit("central")
.build();
}
@Primary
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager (@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
对于包@Entity
中的customer
,我们使用以下内容设置动态数据源解析器@Configuration
。
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "customerEntityManagerFactory",
transactionManagerRef = "customerTransactionManager",
basePackages = { "com.ft.model.customer" }
)
public class CustomerDatabaseConfiguration {
@Bean(name = "customerDataSource")
public DataSource dataSource() {
return new MultitenantDataSourceResolver();
}
@Bean(name = "customerEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("customerDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.customer")
.persistenceUnit("customer")
.build();
}
@Bean(name = "customerTransactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("customerEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
在MultitenantDataSourceResolver
课堂上,我们计划维护一个Map
created DataSource
using customerCode
as key。从每个传入的请求中,我们将获取customerCode
并将其注入到我们的方法MultitenantDataSourceResolver
中以获取正确DataSource
的determineTargetDataSource()
方法。
public class MultitenantDataSourceResolver extends AbstractRoutingDataSource {
@Autowired
private Provider<CustomerWrapper> customerWrapper;
private static final Map<String, DataSource> dsCache = new HashMap<String, DataSource>();
@Override
protected Object determineCurrentLookupKey() {
try {
return customerWrapper.get().getCustomerCode();
} catch (Exception ex) {
return null;
}
}
@Override
protected DataSource determineTargetDataSource() {
String customerCode = (String) this.determineCurrentLookupKey();
if (customerCode == null)
return MultitenantDataSourceResolver.getDefaultDataSource();
else {
DataSource dataSource = dsCache.get(customerCode);
if (dataSource == null)
dataSource = this.buildDataSourceForCustomer();
return dataSource;
}
}
private synchronized DataSource buildDataSourceForCustomer() {
CustomerWrapper wrapper = customerWrapper.get();
if (dsCache.containsKey(wrapper.getCustomerCode()))
return dsCache.get(wrapper.getCustomerCode() );
else {
DataSource dataSource = DataSourceBuilder.create(MultitenantDataSourceResolver.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url(wrapper.getJdbcUrl())
.username(wrapper.getDbUsername())
.password(wrapper.getDbPassword())
.build();
dsCache.put(wrapper.getCustomerCode(), dataSource);
return dataSource;
}
}
private static DataSource getDefaultDataSource() {
return DataSourceBuilder.create(CustomerDatabaseConfiguration.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
}
CustomerWrapper
是一个对象,其@RequestScope
值将由@Controller
. 我们使用java.inject.Provider
将其注入到我们的MultitenantDataSourceResolver
.
最后,即使从逻辑上讲,我们永远不会使用默认值保存任何内容,DataSource
因为所有请求都将始终包含一个customerCode
,在启动时,没有customerCode
可用的。因此,我们仍然需要提供一个有效的 default DataSource
。否则,应用程序将无法启动。
如果您有任何意见或更好的解决方案,请告诉我。
推荐阅读
- sql-server - 确定最终的 Windows 用户
- artifactory - jfrog rt glc 在 refs/remotes/* 中搜索匹配的工件存储库文件。git lfs 将这些文件放在 lfs\objects
- xslt - 尝试在结果文档中删除元素并重新排序元素
- javascript - 防止 VueX 中的持久状态锁定应用程序
- python - 尽管 em 在 $PATH 中,Sphinx 找不到模块
- ios - 将文本设置为 shouldChangeCharactersIn 内的 UITextField 后,光标结束
- opencv - 如何通过 OpenCV 验证标记特征是否正确跟踪视频中的对象?
- java - Maven Clover Instrumentation 错误 - 意外令牌
- sql - 如何使用循环在sql表中输入数据
- javascript - 删除除单词和单词之间的空格以外的所有内容