Spring - Change transaction isolation level example
Introduction
As we have seen previously on Spring transaction isolation level tutorial, Spring supports the definition of the transaction isolation level in service methods.
What we might not expect at first is that when we set the transaction isolation level to any level that is not the deafult one, and use JPA, the following exception will be generated:
at org.springframework.orm.jpa.DefaultJpaDialect.beginTransaction(DefaultJpaDialect.java:67)
at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:378)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:372)
at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:417)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:255)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
In this article we will see how to properly set the transaction isolation level together with JPA in Spring.
The following environment is considered:
- Ubuntu 12.04
- JDK 1.7.0.21
- Spring 3.2.5
- Hibernate 4.1.9
Setup
We will use the same setup, ie. Entity, DAO and Service we used before in the following article Spring + JPA + Hibernate example. In this previous article we have seen how to use Spring with plain JPA and we will reuse its components. This way we may now keep our focus on changing the transaction isolation level.
The main difference is that we will set the isolation level of our service method findAllUsers to READ_UNCOMMITTED. This means that the method will read information written by other transactions that is not yet committed:
package com.byteslounge.spring.tx.user.impl; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.byteslounge.spring.tx.dao.UserDAO; import com.byteslounge.spring.tx.model.User; import com.byteslounge.spring.tx.user.UserManager; @Service public class UserManagerImpl implements UserManager { @Autowired private UserDAO userDAO; @Override @Transactional public void insertUser(User user) { userDAO.insertUser(user); } @Override @Transactional(isolation = Isolation.READ_UNCOMMITTED) public List<User> findAllUsers() { return userDAO.findAllUsers(); } }
You may set the transaction isolation level to any value you need. We will use READ_UNCOMMITTED in this article.
Custom JpaDialect
In order to properly change the Spring transaction isolation level he have to define a custom JPA dialect that extends an already existing JPA dialect.
The main idea is to override the methods that are needed to begin the transaction and change the isolation level. All other methods will still be implemented by the Spring provided JPA dialect (the class we are extending).
Since we are using Hibernate in this article we need to extend HibernateJpaDialect:
package com.byteslounge.spring.tx.dialect; import java.sql.Connection; import java.sql.SQLException; import javax.persistence.EntityManager; import javax.persistence.PersistenceException; import org.hibernate.Session; import org.hibernate.jdbc.Work; import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.orm.jpa.vendor.HibernateJpaDialect; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; public class CustomHibernateJpaDialect extends HibernateJpaDialect { private static final long serialVersionUID = 1L; @Override public Object beginTransaction(final EntityManager entityManager, final TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException { Session session = (Session) entityManager.getDelegate(); if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) { getSession(entityManager).getTransaction().setTimeout( definition.getTimeout()); } final TransactionData data = new TransactionData(); session.doWork(new Work() { @Override public void execute(Connection connection) throws SQLException { Integer previousIsolationLevel = DataSourceUtils .prepareConnectionForTransaction(connection, definition); data.setPreviousIsolationLevel(previousIsolationLevel); data.setConnection(connection); } }); entityManager.getTransaction().begin(); Object springTransactionData = prepareTransaction(entityManager, definition.isReadOnly(), definition.getName()); data.setSpringTransactionData(springTransactionData); return data; } @Override public void cleanupTransaction(Object transactionData) { super.cleanupTransaction(((TransactionData) transactionData) .getSpringTransactionData()); ((TransactionData) transactionData).resetIsolationLevel(); } private static class TransactionData { private Object springTransactionData; private Integer previousIsolationLevel; private Connection connection; public TransactionData() { } public void resetIsolationLevel() { if (this.previousIsolationLevel != null) { DataSourceUtils.resetConnectionAfterTransaction(connection, previousIsolationLevel); } } public Object getSpringTransactionData() { return this.springTransactionData; } public void setSpringTransactionData(Object springTransactionData) { this.springTransactionData = springTransactionData; } public void setPreviousIsolationLevel(Integer previousIsolationLevel) { this.previousIsolationLevel = previousIsolationLevel; } public void setConnection(Connection connection) { this.connection = connection; } } }
We are overriding a couple of methods.
beginTransaction: This method should - as the name states - begin a transaction. The method return value (of type Object) will be passed to the also overridden cleanupTransaction method.
Basically we are extracting the transaction properties defined in the Spring service method through the @Transactional annotation and using them were appropriate. For example, we check if a transaction timeout was defined and use it.
We also defined an inner class TransactionData that will be used to keep the transaction information and restore the original connection configuration during clean up (the result returned by beginTransactionmethod we just mentioned before).
Another thing to note is that since we are using Hibernate 4 we must use session.doWork method to get a reference to the underlying connection and prepare it using the transaction definition information. The transaction definition contains the desired isolation level that was defined in the Spring service. We also keep the previous isolation level for clean up.
Finally we begin the transaction and prepare the transaction by calling prepareTransaction method from the super class.
cleanupTransaction: This method is responsible for transaction clean up. It will receive the value returned by beginTransaction. Since we stored all the information we need for clean up - including the previous transaction isolation level - it should be trivial to perform the clean up and restore the previous connection isolation level.
Spring configuration
We need to instruct the Entity Manager Factory to use our custom dialect:
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceUnitName" value="testPU" /> <property name="dataSource" ref="dataSource" /> <property name="jpaDialect"> <bean class="com.byteslounge.spring.tx.dialect.CustomHibernateJpaDialect" /> </property> </bean>
And omit the Hibernate dialect we had previously defined on the Persistence Unit configuration:
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> <persistence-unit name="testPU" transaction-type="RESOURCE_LOCAL"> <class>com.byteslounge.spring.tx.model.User</class> <provider>org.hibernate.ejb.HibernatePersistence</provider> </persistence-unit> </persistence>
Testing
We will use the following class in order to test our transaction configuration:
package com.byteslounge.spring.tx; import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.byteslounge.spring.tx.model.User; import com.byteslounge.spring.tx.user.UserManager; public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "spring.xml"); UserManager userManager = (UserManager) ctx.getBean("userManagerImpl"); List<User> list = userManager.findAllUsers(); System.out.println("User count: " + list.size()); } }
We have no records in the USER table:
When we run our application the following output will be generated:
Now we go to MySQL console, start a transaction manually and insert a new user without committing the transaction:
INSERT INTO USER(ID, USERNAME, NAME) VALUES (1000,'john','John');
We run the application again and the following output is generated:
Now we go back to MySQL console and rollback the transaction:
We run the application again:
As we can see our method fetched information that was not yet committed by the other concurrent transaction so the READ_UNCOMMITTED isolation level produced the expected result.
Downloadable sample
You may find the fully working sample available for download at the end of this page.