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 作为键来维护创建的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。否则,应用程序将无法启动。

如果您有任何 cmets 或更好的解决方案,请告诉我。

【讨论】:

以上是关于Spring JPA:如何在同一个请求中更新 2 个不同的“DataSource”中的 2 个不同的表?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Controller 的 java spring JPA 中进行批量更新

如何在 Spring Data 中漂亮地更新 JPA 实体?

Spring JPA 更新不适用于嵌套对象

Spring JPA 锁定概念

Spring Boot JPA:@Modifying @Query 没有效果

如何使用条件(where子句)更新实体并在spring数据jpa中的方法响应中获取更新的实体