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

柔情痞子 提交于 2020-01-03 03:14:08

问题


I'm building a spring boot application that has multiple datasources, entity managers, transaction managers and databases. Each one is for a customer and share same DAOs, Services.

The swicth betweeen datasource work perfectly. But I have problem with transaction

Here my configuration:

package org.foo.config;

@Configuration
@EnableJpaRepositories(basePackages = "org.foo")
@EnableTransactionManagement
public class DataSourceConfiguration
{

@Value("#{'${load.datasources}'.split(',')}")
private List<String> toLoadDatasources;

@Value("${default.datasource}")
private String defaultDatasource;

@Bean
@ConfigurationProperties("spring.jpa")
public JpaProperties jpaProperties()
{
    return new JpaProperties();
}

@Bean
@Primary
public DataSource dataSource()
{
    if(toLoadDatasources.isEmpty())
    {
        throw new IllegalArgumentException("At least one datasource to load must be provided. Please check datasources configuration");
    }
    if(defaultDatasource == null || defaultDatasource.isEmpty())
    {
        throw new IllegalArgumentException("No default datasource provided. Please check datasources configuration");
    }
    if(!toLoadDatasources.contains(defaultDatasource))
    {
        throw new IllegalArgumentException("Default datasource must appear in the list of datasources to load. Please check datasources configuration");
    }

    final Map<Object, Object> map = new HashMap<Object, Object>();

    if(toLoadDatasources.contains(Customer.CUST1.name()))
    {
        map.put("datasourceCust1", dataSourceCust1());
    }
    if(toLoadDatasources.contains(Customer.CUST2.name()))
    {
        map.put("datasourceCust2", dataSourceCust2());
    }
    if(toLoadDatasources.contains(Customer.CUST3.name()))
    {
        map.put("datasourceCust3", dataSourceCust3());
    }
    if(toLoadDatasources.contains(Customer.CUST4.name()))
    {
        map.put("datasourceCust4", dataSourceCust4());
    }

    DataSourceRouter router = new DataSourceRouter();
    router.setTargetDataSources(map);

    if(Customer.CUST1.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust1());
    }
    else if(Customer.CUST2.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust2());
    }
    else if(Customer.CUST3.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust3());
    }
    else if(Customer.CUST4.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust4());
    }
    else
    {
        throw new IllegalArgumentException("At least one default datasource must be provided.");
    }

    return router;
}

@Bean
@Primary
public LocalContainerEntityManagerFactoryBean emfb(DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .build();
}

@Bean
@Primary
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfb(dataSource(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust1")
@Conditional(LoadCust1DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust1.datasource")
public DataSource dataSourceCust1()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust1")
@Bean(name="entityManagerCust1")
@Conditional(LoadCust1DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust1(DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust1")
            .build();
}

@Bean(name="transactionManagerCust1")
@Conditional(LoadCust1DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust1(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust1(dataSourceCust1(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust2")
@Conditional(LoadCust2DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust2.datasource")
public DataSource dataSourceCust2()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust2")
@Bean(name="entityManagerCust2")
@Conditional(LoadCust2DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust2(@Qualifier("dataSourceCust2") DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust2")
            .build();
}

@Bean(name="transactionManagerCust2")
@Conditional(LoadCust2DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust2(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust2(dataSourceCust2(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust3")
@Conditional(LoadCust3DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust3.datasource")
public DataSource dataSourceCust3()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust3")
@Bean(name="entityManagerCust3")
@Conditional(LoadCust3DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust3(@Qualifier("dataSourceCust3") DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust3")
            .build();
}

@Bean(name="transactionManagerCust3")
@Conditional(LoadCust3DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust3(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust3(dataSourceCust3(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust4")
@Conditional(LoadCust4DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust4.datasource")
public DataSource dataSourceCust4()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust4")
@Bean(name="entityManagerCust4")
@Conditional(LoadCust4DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust4(@Qualifier("dataSourceCust4") DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust4")
            .build();
}

@Bean(name="transactionManagerCust4")
@Conditional(LoadCust4DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust4(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust4(dataSourceCust4(), builder, jpaProperties).getObject());
    return transactionManager;
}

}

The datasources are loaded depending on the configuration file. The classes like LoadCust4DatasourceCondition are used to check if to load or not.

My datasources configuration file is:

 # Datasources
 spring.cust1.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust1.datasource.url: 
 spring.cust1.datasource.username: root
 spring.cust1.datasource.password: pass

 spring.cust2.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust2.datasource.url: 
 spring.cust2.datasource.username: root
 spring.cust2.datasource.password: pass

 spring.cust3.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust3.datasource.url: 
 spring.cust3.datasource.username: root
 spring.cust3.datasource.password: pass

 spring.cust4.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust4.datasource.url: jdbc:
 spring.cust4.datasource.username: root
 spring.cust4.datasource.password: pass


 # JPA/Hibernate
 spring.jpa.hibernate.dialect: org.hibernate.dialect.MySQL5Dialect
 spring.jpa.hibernate.show_sql: true
 spring.jpa.hibernate.hbm2ddl.auto: none
 spring.jpa.entitymanager.packagesToScan: org.foo.domain

 load.datasources: CUST1, CUST2, CUST3, CUST4
 default.datasource: CUST1

My Service is like:

 @Service
 public class InvoiceServiceImpl implements IInvoiceService {
  @Autowired
  private IInvoiceDao invoiceDao;

  @Override
  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {TechnicalException.class, BusinessException.class}, transactionManager = "transactionManagerCust1")
  public Invoice create(Invoice invoice, Customer customer) throws AbstractException {

    return invoiceDao.persist(invoice, customer);
}

}

My dao is like:

@Repository
public class InvoiceDaoImpl implements IInvoiceDao
{

@Autowired(required = false)
@Qualifier("entityManagerCust1")
private EntityManager entityManagerCust1;

@Autowired(required = false)
@Qualifier("entityManagerCust2")
private EntityManager entityManagerCust2;

@Autowired(required = false)
@Qualifier("entityManagerCust3")
private EntityManager entityManagerCust3;

@Autowired(required = false)
@Qualifier("entityManagerCust4")
private EntityManager entityManagerCust4;

@Override
public Invoice persist(Invoice invoice, Customer customer) throws AbstractException {

    try {
        getEntityManager(customer).persist(invoice);
    } catch(EntityExistsException eee) {
        logger.error(ExceptionConstantes.MSG_INV_ALRDY_EXIST);
        throw new BusinessException(ExceptionConstantes.MSG_INV_ALRDY_EXIST, ExceptionConstantes.CODE_INV_ALRDY_EXIST);
    }catch (Exception e){
        logger.error(String.format(ExceptionConstantes.MSG_CREATE_ERR, invoice.getClass().getSimpleName()), e);
        throw new TechnicalException(String.format(ExceptionConstantes.MSG_CREATE_ERR, invoice.getClass().getSimpleName()));
    }

    return invoice;
}




private EntityManager getEntityManager(Customer customer) throws IllegalArgumentException
{
    switch(customer)
    {
        case CUST1 : if(entityManagerCust1 == null){ throw new IllegalArgumentException("Requested " + customer.name() +"'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust1;
        case CUST2 : if(entityManagerCust2 == null){ throw new IllegalArgumentException("Requested " + customer.name() + "'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust2;
        case CUST3 : if(entityManagerCust3 == null){ throw new IllegalArgumentException("Requested " + customer.name() + "'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust3;
        case CUST4 : if(entityManagerCust4 == null){ throw new IllegalArgumentException("Requested " + customer.name() + "'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust4;
        default: throw new IllegalArgumentException("Invalid customer " + customer.name());
    }
}


@Conditional(LoadCust1DatasourceCondition.class)
public void setEntityManagerCust1(EntityManager entityManagerCust1) 
{
    this.entityManagerCust1 = entityManagerCust1;
}

@Conditional(LoadCust2DatasourceCondition.class)
public void setEntityManagerCust2(EntityManager entityManagerCust2) 
{
    this.entityManagerCust2 = entityManagerCust2;
}
@Conditional(LoadCust3DatasourceCondition.class)
public void setEntityManagerCust3(EntityManager entityManagerCust3) 
{
    this.entityManagerCust3 = entityManagerCust3;
}
@Conditional(LoadCust4DatasourceCondition.class)
public void setEntityManagerCust4(EntityManager entityManagerCust4) 
{
    this.entityManagerCust4 = entityManagerCust4;
}

In the service level, If the transactionManager attribute of @Transactional is not set with the underlying customer transactionManager bean the persist method of EntityManager does not persist to database. I want a dynamic changing of this value depending on the used Datasource/EntityManager.

Or a global transaction manager, but without transaction issue if all customers are using the same services and DAOs in the same time.

The customer is determined in Webservice layer that use Invoice Service.

Thanks for your reponse.


回答1:


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.




回答2:


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.



来源:https://stackoverflow.com/questions/48562425/manage-transactions-with-multiple-datasource-entity-managers-for-same-applicati

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