Spring JPA:如何在同一请求中更新2个不同`DataSource`中的2个不同表?

时间:2018-12-13 04:00:54

标签: java spring spring-boot spring-data-jpa multi-tenant

在我们的应用程序中,我们有一个名为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的更新。

1 个答案:

答案 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。否则,该应用程序将无法启动。

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