在我们的应用程序中,我们有一个名为central
的公共数据库,每个客户都有一个拥有完全相同的表集的数据库。每个客户的数据库都可以根据客户组织的要求托管在我们自己的服务器或客户的服务器上。
为处理此多租户要求,我们从Spring JPA扩展了AbstractRoutingDataSource
并覆盖了determineTargetDataSource()
方法以创建新的DataSource
并即时建立新的连接基于传入的customerCode
。我们还使用一个简单的DatabaseContextHolder
类将当前数据源上下文存储在ThreadLocal
变量中。我们的解决方案类似于此article中描述的解决方案。
假设在一个请求中,我们将需要更新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()
时,此代码才起作用,以便我们可以在方法中途动态切换DataSource
。
但是,在此请求中首次检索到determineTargetDataSource()
时,似乎从该Stackoverflow question中仅对每个HttpRequest
调用一次DataSource
如果您能给我一些有关AbstractRoutingDataSource.determineTargetDataSource()
实际何时致电的见解,我将不胜感激。此外,如果您之前曾处理过类似的多租户方案,那么我很想听听您的意见,即我应如何在单个请求中处理多个DataSource
的更新。
答案 0 :(得分:0)
我们找到了一个可行的解决方案,其中包括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);
}
}
对于@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
作为键维护创建的DataSource
的{{1}}。从每个传入的请求中,我们将获得customerCode
并将其注入到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
。否则,该应用程序将无法启动。
如果您有任何意见或更好的解决方案,请告诉我。