Spring JPA:如何在同一个请求中更新 2 个不同的“DataSource”中的 2 个不同的表?
Posted
技术标签:
【中文标题】Spring JPA:如何在同一个请求中更新 2 个不同的“DataSource”中的 2 个不同的表?【英文标题】:Spring JPA: How to update 2 different tables in 2 different `DataSource` in the same request? 【发布时间】:2019-05-14 06:07:51 【问题描述】:在我们的应用程序中,我们有一个名为central
的通用数据库,每个客户都有自己的数据库,其中包含完全相同的表集。每个客户的数据库可以托管在我们自己的服务器上,也可以根据客户组织的要求托管在客户的服务器上。
为了处理这种多租户需求,我们从 Spring JPA 扩展 AbstractRoutingDataSource
并覆盖 determineTargetDataSource()
方法以创建新的 DataSource
并根据传入的 @987654328 即时建立新连接@。我们还使用一个简单的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
只有在每次就在执行任何 SQL 查询时调用 determineTargetDataSource()
时,此代码才有效,以便我们可以在方法的中途动态切换 DataSource
。
但是,从这个*** question 看来,当在该请求中第一次检索DataSource
时,似乎每个HttpRequest
只调用一次determineTargetDataSource()
。
如果您能给我一些关于AbstractRoutingDataSource.determineTargetDataSource()
何时真正被调用的见解,我将不胜感激。此外,如果您之前处理过类似的多租户场景,我很想听听您对我应该如何处理在单个请求中更新多个 DataSource
的意见。
【问题讨论】:
请问您使用的是哪个 Spring Boot 版本? @IanLim:我使用的是 2.1.1 版本 :) 看看***.com/questions/27614301/…能不能帮上忙。主要思想-利用@Primary注解 【参考方案1】:我们找到了一个可行的解决方案,它混合了我们的 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);
对于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
作为键来维护创建的DataSource
的Map
。从每个传入的请求中,我们将获取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
。否则,应用程序将无法启动。
如果您有任何 cmets 或更好的解决方案,请告诉我。
【讨论】:
以上是关于Spring JPA:如何在同一个请求中更新 2 个不同的“DataSource”中的 2 个不同的表?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Controller 的 java spring JPA 中进行批量更新
如何在 Spring Data 中漂亮地更新 JPA 实体?