Spring JPA:如何在同一请求中更新两个不同"DataSource"中的两个不同表



在我们的应用程序中,我们有一个名为central的公共数据库,每个客户都有自己的数据库,其中包含完全相同的表集。根据客户组织的要求,每个客户的数据库可能托管在我们自己的服务器上,也可能托管在客户的服务器上。

为了处理这个多租户需求,我们从Spring JPA扩展了AbstractRoutingDataSource,并覆盖了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
}

只有在执行任何SQL查询之前每次都调用determineTargetDataSource(),这样我们就可以在方法执行到一半时动态切换DataSource,此代码才会起作用。

然而,从这个堆栈溢出问题来看,当在该请求中第一次检索DataSource时,似乎每个HttpRequest只调用determineTargetDataSource()一次。

如果你能给我一些关于AbstractRoutingDataSource.determineTargetDataSource()实际何时被呼叫的见解,我将不胜感激。此外,如果您以前处理过类似的多租户场景,我很想听听您对如何处理在一个请求中更新多个DataSource的意见。

我们找到了一个有效的解决方案,它混合了central数据库的静态数据源设置和客户数据库的动态数据源设置。

从本质上讲,我们确切地知道哪个表来自哪个数据库。因此,我们能够将我们的@Entity类分为两个不同的包,如下所示。

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);
}
}

对于customer包中的@Entity,我们使用以下@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类中,我们计划使用customerCode作为密钥来维护创建的DataSourceMap。从每个传入的请求中,我们将获得customerCode,并将其注入到我们的MultitenantDataSourceResolver中,以在determineTargetDataSource()方法中获得正确的DataSource

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。因此,我们仍然需要提供一个有效的默认DataSource。否则,应用程序将无法启动。

如果你有任何意见或更好的解决方案,请告诉我。

最新更新