首页 > 解决方案 > Spring JPA:如何在同一个请求中更新 2 个不同的“DataSource”中的 2 个不同的表?

问题描述

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

为了处理这种多租户需求,我们扩展了AbstractRoutingDataSourceSpring JPA 并重写了基于传入的动态determineTargetDataSource()创建新连接和建立新连接的方法。我们还使用一个简单的类将当前数据源上下文存储在一个变量中。我们的解决方案与本文中描述的类似。DataSourcecustomerCodeDatabaseContextHolderThreadLocal

假设在一个请求中,我们需要更新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()只有在每次执行任何 SQL 查询之前都调用此代码才能工作,以便我们可以在方法的DataSource中途动态切换。

但是,从这个Stackoverflow question看来,当在该请求中第一次检索a 时,似乎determineTargetDataSource()只调用一次。HttpRequestDataSource

AbstractRoutingDataSource.determineTargetDataSource()如果您能给我一些关于何时实际被调用的见解,我将非常感激。此外,如果您之前处理过类似的多租户场景,我很想听听您对我应该如何处理DataSource单个请求中的多个更新的意见。

标签: javaspringspring-bootspring-data-jpamulti-tenant

解决方案


我们找到了一个可行的解决方案,它混合了我们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);
    }
}

对于包@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课堂上,我们计划维护一个Mapcreated DataSourceusing customerCodeas key。从每个传入的请求中,我们将获取customerCode并将其注入到我们的方法MultitenantDataSourceResolver中以获取正确DataSourcedetermineTargetDataSource()方法。

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

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


推荐阅读