Distributed transactions with multiple databases, Spring Boot, Spring Data JPA and Atomikos
2014-09-14 18:39
495 查看
A couple of weeks ago I was evaluating the possibility to use Spring Boot, Spring Data JPA and Atomikos for distributed transactions involving multiple databases. After looking at the
Spring blog article (which involves one database and ActiveMQ) and having done some attempts, I could not get it to work with two databases. The configuration seemed fine, but the Entity Manager did not get notified when persisting my entities. So I wrote
this question on StackOverflow, which has been answered directly by
Dave Syer and
Oliver Gierke. This post is to share and discuss the solution.
the second database. The two entities are very simple, as they serve only as a demonstration.
The resulting implementation is the following. It's worth noting that they belong to two different packages, for two main reasons:
it's a logical separation that gives order to the project
each repository will scan packages containing only entities that it will be going to manage
See
Lombok for annotations like
it specifies which are the packages to scan for annotated components (repository interfaces), and in my case I wanted only repositories related to the
customer (and conversely to the order).
it specifies which is the entity manager to be used to manage entities, in my case the
it specifies the transaction manager to be used, in my case the
Another important thing here is the definition of the
the
you need to set some properties to the
Not setting the second property was the reason why I could not get it work. As Dave Syer wrote "It seems Hibernate4 doesn't work with Atomikos out of the box", so you need to implement the class to be set as
StackOverflow discussion about this topic. If you are using another JTA provider, you may find
this useful.
very important thing to notice is that we need to set
You can see the full source code here: https://github.com/fabiomaffioletti/mul-at, The
Spring blog article (which involves one database and ActiveMQ) and having done some attempts, I could not get it to work with two databases. The configuration seemed fine, but the Entity Manager did not get notified when persisting my entities. So I wrote
this question on StackOverflow, which has been answered directly by
Dave Syer and
Oliver Gierke. This post is to share and discuss the solution.
Description of the case and entities model
We want to be able to save two entities at the same time into two different databases; the operation must be transactional. So, in this example, we have a Customer entity, which is persisted in the first database, and an Order entity which is persisted inthe second database. The two entities are very simple, as they serve only as a demonstration.
The resulting implementation is the following. It's worth noting that they belong to two different packages, for two main reasons:
it's a logical separation that gives order to the project
each repository will scan packages containing only entities that it will be going to manage
package com.at.mul.domain.customer; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; @Entity @Table(name = "customer") @Data @EqualsAndHashCode(exclude = { "id" }) public class Customer { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @Column(name = "name", nullable = false) private String name; @Column(name = "age", nullable = false) private Integer age; }
package com.at.mul.domain.order; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; @Entity @Table(name = "orders") @Data @EqualsAndHashCode(exclude = { "id" }) public class Order { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @Column(name = "code", nullable = false) private Integer code; @Column(name = "quantity", nullable = false) private Integer quantity; }
See
Lombok for annotations like
@Dataand
@EqualsAndHashCode
Write repositories interfaces
Also in this case it's standard, the only thing to notice is that I put the two interfaces in two different packages. The reason is explained in the next step.package com.at.mul.repository.customer; import org.springframework.data.jpa.repository.JpaRepository; import com.at.mul.domain.customer.Customer; public interface CustomerRepository extends JpaRepository<Customer, Integer> { }
package com.at.mul.repository.order; import org.springframework.data.jpa.repository.JpaRepository; import com.at.mul.domain.order.Order; public interface OrderRepository extends JpaRepository<Order, Integer> { }
Write configuration classes
This is where it becomes interesting. The@DependsOn("transactionManager")annotation is not mandatory, but I needed this to get rid of several warnings at tests (or application) startup, like
WARNING: transaction manager not running?in the logs. The next annotation
@EnableJpaRepositoriesis more important:
it specifies which are the packages to scan for annotated components (repository interfaces), and in my case I wanted only repositories related to the
customer (and conversely to the order).
it specifies which is the entity manager to be used to manage entities, in my case the
customerEntityManagerfor customer related operations and
orderEntityManagerfor order related operations
it specifies the transaction manager to be used, in my case the
transactionManagerdefined in the
MainConfigclass. This needs to be the same for every
@EnableJpaRepositoriesto get distributed transactions working
package com.at.mul; import java.util.HashMap; import javax.sql.DataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import com.at.mul.repository.customer.CustomerDatasourceProperties; import com.atomikos.jdbc.AtomikosDataSourceBean; @Configuration @DependsOn("transactionManager") @EnableJpaRepositories(basePackages = "com.at.mul.repository.customer", entityManagerFactoryRef = "customerEntityManager", transactionManagerRef = "transactionManager") @EnableConfigurationProperties(CustomerDatasourceProperties.class) public class CustomerConfig { @Autowired private JpaVendorAdapter jpaVendorAdapter; @Autowired private CustomerDatasourceProperties customerDatasourceProperties; @Bean(name = "customerDataSource", initMethod = "init", destroyMethod = "close") public DataSource customerDataSource() { JdbcDataSource h2XaDataSource = new JdbcDataSource(); h2XaDataSource.setURL(customerDatasourceProperties.getUrl()); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(h2XaDataSource); xaDataSource.setUniqueResourceName("xads1"); return xaDataSource; } @Bean(name = "customerEntityManager") @DependsOn("transactionManager") public LocalContainerEntityManagerFactoryBean customerEntityManager() throws Throwable { HashMap<String, Object> properties = new HashMap<String, Object>(); properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName()); properties.put("javax.persistence.transactionType", "JTA"); LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean(); entityManager.setJtaDataSource(customerDataSource()); entityManager.setJpaVendorAdapter(jpaVendorAdapter); entityManager.setPackagesToScan("com.at.mul.domain.customer"); entityManager.setPersistenceUnitName("customerPersistenceUnit"); entityManager.setJpaPropertyMap(properties); return entityManager; } }
package com.at.mul; import java.util.HashMap; import javax.sql.DataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import com.at.mul.repository.order.OrderDatasourceProperties; import com.atomikos.jdbc.AtomikosDataSourceBean; @Configuration @DependsOn("transactionManager") @EnableJpaRepositories(basePackages = "com.at.mul.repository.order", entityManagerFactoryRef = "orderEntityManager", transactionManagerRef = "transactionManager") @EnableConfigurationProperties(OrderDatasourceProperties.class) public class OrderConfig { @Autowired private JpaVendorAdapter jpaVendorAdapter; @Autowired private OrderDatasourceProperties orderDatasourceProperties; @Bean(name = "orderDataSource", initMethod = "init", destroyMethod = "close") public DataSource orderDataSource() { JdbcDataSource h2XaDataSource = new JdbcDataSource(); h2XaDataSource.setURL(orderDatasourceProperties.getUrl()); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(h2XaDataSource); xaDataSource.setUniqueResourceName("xads2"); return xaDataSource; } @Bean(name = "orderEntityManager") public LocalContainerEntityManagerFactoryBean orderEntityManager() throws Throwable { HashMap<String, Object> properties = new HashMap<String, Object>(); properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName()); properties.put("javax.persistence.transactionType", "JTA"); LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean(); entityManager.setJtaDataSource(orderDataSource()); entityManager.setJpaVendorAdapter(jpaVendorAdapter); entityManager.setPackagesToScan("com.at.mul.domain.order"); entityManager.setPersistenceUnitName("orderPersistenceUnit"); entityManager.setJpaPropertyMap(properties); return entityManager; } }
Another important thing here is the definition of the
LocalContainerEntityManagerFactoryBean.
the
@Beanannotation has a given
name, that is the one specified in the
@EnableJpaRepositoriesannotation.
you need to set some properties to the
JpaPropertyMap, in particular you need to say that the transaction type is JTA and that the jta platform is
AtomikosJtaPlatform.class.getName()
Not setting the second property was the reason why I could not get it work. As Dave Syer wrote "It seems Hibernate4 doesn't work with Atomikos out of the box", so you need to implement the class to be set as
hibernate.transaction.jta.platformproperty by yourself. In my opinion this is not very well documented, but fortunately Oliver Gierke found another
StackOverflow discussion about this topic. If you are using another JTA provider, you may find
this useful.
Write the AbstractJtaPlatform implementation
As said, this is the most important step, as we need to write the implementation of that class by ourselves since Hibernate does not provide it. Here is the resulting code:package com.at.mul; import javax.transaction.TransactionManager; import javax.transaction.UserTransaction; import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform; public class AtomikosJtaPlatform extends AbstractJtaPlatform { private static final long serialVersionUID = 1L; static TransactionManager transactionManager; static UserTransaction transaction; @Override protected TransactionManager locateTransactionManager() { return transactionManager; } @Override protected UserTransaction locateUserTransaction() { return transaction; } }
Write the main configuration class
Also in this case it's a pretty standard class, with@EnableTransactionManagementannotation and Atomikos bean definitions. The only
very important thing to notice is that we need to set
AtomikosJtaPlatform.transactionManagerand
AtomikosJtaPlatform.transactionattributes.
package com.at.mul; import javax.transaction.TransactionManager; import javax.transaction.UserTransaction; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.jta.JtaTransactionManager; import com.atomikos.icatch.jta.UserTransactionImp; import com.atomikos.icatch.jta.UserTransactionManager; @Configuration @ComponentScan @EnableTransactionManagement public class MainConfig { @Bean public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } @Bean public JpaVendorAdapter jpaVendorAdapter() { HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter(); hibernateJpaVendorAdapter.setShowSql(true); hibernateJpaVendorAdapter.setGenerateDdl(true); hibernateJpaVendorAdapter.setDatabase(Database.H2); return hibernateJpaVendorAdapter; } @Bean(name = "userTransaction") public UserTransaction userTransaction() throws Throwable { UserTransactionImp userTransactionImp = new UserTransactionImp(); userTransactionImp.setTransactionTimeout(10000); return userTransactionImp; } @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close") public TransactionManager atomikosTransactionManager() throws Throwable { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); AtomikosJtaPlatform.transactionManager = userTransactionManager; return userTransactionManager; } @Bean(name = "transactionManager") @DependsOn({ "userTransaction", "atomikosTransactionManager" }) public PlatformTransactionManager transactionManager() throws Throwable { UserTransaction userTransaction = userTransaction(); AtomikosJtaPlatform.transaction = userTransaction; TransactionManager atomikosTransactionManager = atomikosTransactionManager(); return new JtaTransactionManager(userTransaction, atomikosTransactionManager); } }
Resources
Here is the resulting structure of the project:You can see the full source code here: https://github.com/fabiomaffioletti/mul-at, The
masterbranch uses in memory database. Checkout branch named
mysql-dbto use real databases (see
application.propertiesto tweak your database connection data).
相关文章推荐
- Distributed transactions with multiple databases, Spring Boot, Spring Data JPA and Atomikos
- How to setup multiple data sources with Spring and JPA
- How to setup multiple data sources with Spring and JPA
- Distributed transactions in Spring, with and without XA
- Spring Data repositories with multiple databases
- Spring boot and Spring Data JPA 超简单例子
- Spring Boot with Spring Data JPA (1) - Concept
- Spring JTA multiple resource transactions in Tomcat with Atomikos example
- Spring JTA multiple resource transactions in Tomcat with Atomikos example
- spring.data.jpa.query :Cannot use native queries with dynamic sorting and/or pagination in method
- 两张表(多对多关联)操作 with Spring Boot, Spring Data JPA, H2
- Spring boot data JPA 自定义JPQL语句,以及PagingAndSortingRepository接口实现分页查询
- spring boot and spring data jpa
- Distributed transactions in Spring, with and without XA
- Spring Boot中使用Spring-data-jpa
- 【Spring-boot多数据库】Spring-boot JDBC with multiple DataSources sample
- SPRING IN ACTION 第4版笔记-第十一章Persisting data with object-relational mapping-002设置JPA的EntityManagerFactory(<persistence-unit>、<jee:jndi-lookup>)
- Spring with Hibernate persistence and transactions
- Spring Boot中使用Spring-data-jpa让数据访问更简单、更优雅
- Spring Boot+Spring Data JPA+Spring Cache