问题
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