如何使用自定义数据源动态连接存储库?

How to dynamically wire up Repositories with custom DataSources?

所以我有一个相当标准的 Spring 启动应用程序,它使用 JavaConfig 和 JPA 来连接服务和存储库。但是,该应用程序的一个非标准方面是出于法律原因要求按需启动独立的云数据库以保持客户端数据独立。

我有一个注入了一些存储库的简单 ClientService,我的目标是创建某种类型的工厂,我可以在其中请求此 ClientService 的一个版本,特定于每个客户端,它有一个自定义数据源注入到所有存储库。 Spring 启动,通常非常适合连接事物,却让事情变得更加混乱,因为很难看到幕后发生的事情。

解决这个问题的最佳方法是什么?我的第一个想法是使用方法 getClientService(Client client) 的名为 ClientServiceFactory 的 bean。为这个客户端创建自定义数据源对我来说很容易 - 困难的部分是我如何 return ClientService 的实例,自动注入所有其他东西,但强制所有存储库 bean 使用这个数据源.自然地,ClientService 将不再是单例,而是我会在 Client > ClientService 内部存储一个映射。

如有任何帮助或建议,我们将不胜感激。

我相信您需要实施多租户方法。

我在以下位置发布了有关此主题的博客:Multi-tenant applications using Spring Boot, JPA, Hibernate and Postgres

基本上,这是为多租户配置持久层的步骤:

  • Hibernate、JPA 和数据源属性:

application.yml

...
multitenancy:
  dvdrental:
    dataSources:
      -
        tenantId: TENANT_01
        url: jdbc:postgresql://172.16.69.133:5432/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
      -
        tenantId: TENANT_02
        url: jdbc:postgresql://172.16.69.133:5532/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
...

MultiTenantJpaConfiguration.java

 ...
 @Configuration
 @EnableConfigurationProperties({ MultiTenantDvdRentalProperties.class, JpaProperties.class })
 @ImportResource(locations = { "classpath:applicationContent.xml" })
 @EnableTransactionManagement
 public class MultiTenantJpaConfiguration {

   @Autowired
   private JpaProperties jpaProperties;

   @Autowired
   private MultiTenantDvdRentalProperties multiTenantDvdRentalProperties;
 ...
 }

MultiTenantDvdRentalProperties.java

...
@Configuration
@ConfigurationProperties(prefix = "multitenancy.dvdrental")
public class MultiTenantDvdRentalProperties {

  private List<DataSourceProperties> dataSourcesProps;
  // Getters and Setters

  public static class DataSourceProperties extends org.springframework.boot.autoconfigure.jdbc.DataSourceProperties {

    private String tenantId;
    // Getters and Setters
  }
}
  • 数据源 bean:

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean(name = "dataSourcesDvdRental" )
   public Map<String, DataSource> dataSourcesDvdRental() {
       ...
   }
 ...
 }
  • 实体管理器工厂 bean:

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean
   public MultiTenantConnectionProvider multiTenantConnectionProvider() {
       ...
   }

   @Bean
   public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
       ...
   }

   @Bean
   public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider multiTenantConnectionProvider,
     CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
       ...  
   }
 ...
 }
  • 事务管理器 bean:

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean
   public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
       ...
   }

   @Bean
   public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
       ...
   }
 ...
 }
  • Spring数据JPA和事务支持配置:

applicationContent.xml

...
<jpa:repositories base-package="com.asimio.dvdrental.dao" transaction-manager-ref="txManager" />
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" />
...

ActorDao.java

public interface ActorDao extends JpaRepository<Actor, Integer> {
}

根据您的需要,可以这样做:

...
@Autowired
private ActorDao actorDao;
...

DvdRentalTenantContext.setTenantId("TENANT_01");
this.actorDao.findOne(...);
...

// Or
DvdRentalTenantContext.setTenantId("TENANT_02");
this.actorDao.save(...);
...

可以在 servlet 过滤器/Spring MVC 拦截器/线程中通过 ThreadLocal 执行 JPA 操作等来设置 tenantId

您可能无法直接实现此目的。由于 Repositories 与 EntityManagers 紧密耦合,EntityManagers 与 Datasources 紧密耦合,这意味着 Repositories 与 Datasources 紧密耦合。您可能无法在 spring 启动时创建存储库。

您可以在启动时为不同的数据源创建多个 JPARepositorySet。假设您需要一个 UserRepository。您有 2 个数据源 DS1、DS2。

您可以为两个数据源创建两个实体管理器 EM1、EM2。

public LocalContainerEntityManagerFactoryBean em1() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(ds1());
        em.setPackagesToScan(new String[] { "com.pkg1.entities.user" });
        return em;
    }

public LocalContainerEntityManagerFactoryBean em2() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(ds2());
        em.setPackagesToScan(new String[] { "com.pkg1.entities.user" });
        return em;
    }

根据客户端的不同,在运行时,您的工厂 bean 可以为您创建存储库,return 存储库如下所示。

public UserRepository getClientSpecificUserRepository(Client client){
    SimpleJpaRepository<User, Long> userRepository = null;
    if(client.name.equals("ABC")){
        userRepository = new SimpleJpaRepository<User, Serializable>(
    User.class, em1);
    } else (client.name.equals("ABC")){
        userRepository = new SimpleJpaRepository<User, Serializable>(
    User.class, em2);
    }
    return userRepository;
}

因此最终在上下文启动时为所有客户端创建多个 EntityManagers。并且 Factory return 在运行时根据客户端

存储库 bean