Spring - Change transaction isolation level example

02 January 2014
By Gonçalo Marques
In this article we will see how to effectively change Spring transaction isolation level, together with JPA, while avoiding InvalidIsolationLevelException: Standard JPA does not support custom isolation levels - use a special JpaDialect for your JPA implementation.

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:

InvalidIsolationLevelException: Standard JPA does not support custom isolation levels - use a special JpaDialect for your JPA implementation
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:

  1. Ubuntu 12.04
  2. JDK 1.7.0.21
  3. Spring 3.2.5
  4. 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:

UserManagerImpl.java

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:

CustomHibernateJpaDialect.java

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:

spring.xml

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

persistence.xml

<?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:

Main.java

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:

USER table
Empty user table

When we run our application the following output will be generated:

User count: 0

Now we go to MySQL console, start a transaction manually and insert a new user without committing the transaction:

start transaction;
INSERT INTO USER(ID, USERNAME, NAME) VALUES (1000,'john','John');

We run the application again and the following output is generated:

User count: 1

Now we go back to MySQL console and rollback the transaction:

rollback;

We run the application again:

User count: 0

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.

Remember that to run the sample you must have MySQL driver in your application classpath. The application is also using the USER table we defined in a previous article, as we mentioned in the very beginning of this article.

Download source code from this article

Related Articles

Comments

About the author
Gonçalo Marques is a Software Engineer with several years of experience in software development and architecture definition. During this period his main focus was delivering software solutions in banking, telecommunications and governmental areas. He created the Bytes Lounge website with one ultimate goal: share his knowledge with the software development community. His main area of expertise is Java and open source.

GitHub profile: http://github.com/gonmarques

He is also the author of the WiFi File Browser Android application: