Manage transactions with multiple datasource, entity managers for same application code

梦想与她 提交于 2019-12-06 16:39:28
Ján Halaša

I would try to create a custom PlatformTransactionManager which would delegate its calls to the correct transaction manager for the current customer. For that to work, it would have to be able to get the current customer from somewhere - for example from a ThreadLocal variable. Something like this:

public class CustomerAwareTransactionManager implements PlatformTransactionManager {

    // Tx managers beans and their names
    @Autowired 
    private Map<String, PlatformTransactionManager> txManagerMap;

    private PlatformTransactionManager getCurrentManager() {
        // CustomerHolder gets the customer from a ThreadLocal variable
        // something like SecurityContextHolder
        // It should be set just once for a request and removed at the end
        // of each request (to prevent memory leaks)
        String currentIdentifier = CustomerHolder.getCustomer().get().name;
        for (String managerName : txManagerMap.keySet()) {
            if (managerName.equals("transactionManager" + currentIdentifier)) {
                return txManagerMap.get(managerName);
            }
        }
        throw new IllegalStateException("No tx manager for id " + currentIdentifier);
    }
    @Override
    public commit(TransactionStatus status) {
        this.getCurrentManager().commit(status);
    }
    @Override
    public getTransaction(TransactionDefinition definition) {
        this.getCurrentManager().getTransaction(definition);
    }
    @Override
    public rollback(TransactionStatus status) {
        this.getCurrentManager().commit(status);
    }
}

In the DataSourceConfiguration I have replaced the primary transaction manager bean by the following snippet:

 @Bean
@Primary
public PlatformTransactionManager transactionManager()
{
    return new CustomerAwareTransactionManager();
}

And I have created a ThreadLocal variable in the CustomerHolder to store the current Customer:

public class CustomerHolder
{
   private static ThreadLocal<Customer> customer= new ThreadLocal<Customer>();

public static ThreadLocal<Customer> getCustomer() {
    return customer;
}

public static void setCustomer(ThreadLocal<Customer> customer) {
    CustomerHolder.customer= customer;
}
}

In the start of webservice method that calling our Service's create method I store the current customer in CustomerHolder and in the end of same method I remove the current customer to avoid memory leaks.

Then don't use the transactionManager attribute of @Transactional and name this custom transaction manager as transactionManager to make it a default one.

This is an example of a multi-tenancy setup, one application, multiple databases, one DB per client. See my answer at Spring boot - Multiple Database Access (MYSQL)

This is also covered in my blog post at Multi-tenant applications using Spring Boot, JPA, Hibernate and Postgres

Basically to configure the persistence layer for multitenancy support includes:

  • Hibernate, JPA and datasources properties. Something like:

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
...

I used a properties file to store tenants data but this could be adapted to store tenant information in a kind of master DB.

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
  }
}
  • Datasources beans

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean(name = "dataSourcesDvdRental" )
   public Map<String, DataSource> dataSourcesDvdRental() {
       ...
   }
 ...
 }
  • Entity manager factory bean

MultiTenantJpaConfiguration.java

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

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

   @Bean
   public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider multiTenantConnectionProvider,
     CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
       ...  
   }
 ...
 }
  • Transaction manager bean

MultiTenantJpaConfiguration.java

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

   @Bean
   public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
       ...
   }
 ...
 }
  • Spring Data JPA and transaction support configuration

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> {
}

Depending on your needs something like this could be done:

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

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

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

Setting the tenantId could be done in a servlet filter / Spring MVC interceptor / thread that is going to execute the JPA operation, etc.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!