Wednesday, April 11, 2012

Spring JPA Hibernate: Support for custom Isolation Level


While working on deadlock issues in our application, i figured that we need to set custom isolation levels on transactions to avoid the deadlocks. Every thing was good until I encountered a weird behavior of Spring JPA dialect.The story in short is, JPA does not support the custom isolation level for individual transactions. So if you want to set isolation level of transactions individually in @Transactions you get the error from Spring saying:

"Standard JPA does not support custom isolation levels - use a special JpaDialect for your JPA implementation"

The full exception message would look something like:

org.springframework.transaction.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:66)
 at org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction(HibernateJpaDialect.java:55)
 at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:332)
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:371)
 at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:354)
 at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:112)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
 at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)

I googled the problem and found the workaround to this problem here on Stackoverflow and on the Amit's Tech Blog but neither of them account for the cleanup stuff.


Then i delve into the sources of spring and HibernateJpaTemplate to look for the proper cleanup strategy and come up with the solution for cleanup stuff.

Following are the steps to acheive the Custom transaction level support in your application:

  1. Annotate the @Transactional with isolation level you wish to go for i.e. @Transactional(isolation=Isolation.READ_COMMITTED)
  2. Write custom JpaDialect to plugin the transaction's isolation level glue code
  3. Integrate/Inject the custom JpaDialect into the EntityManager using spring context configuration of you application.
Code listing 1: Annotate with custom isolation level
 @Transactional(isolation=Isolation.READ_COMMITTED)
 public void customIsolationTransaction(){
  logger.info("Do something use full") ;
 }

Code listing 2: Customized JpaDialect implementation to support the transaction isolation levels
package com.smughal.spring.orm.jpa;

import java.sql.Connection;
import java.sql.SQLException;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;

import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 IsolationSupportHibernateJpaDialect extends HibernateJpaDialect {

 private Logger logger = LoggerFactory.getLogger(IsolationSupportHibernateJpaDialect.class);

 /**
  * This method is overridden to set custom isolation levels on the
  * connection
  * 
  * @param entityManager
  * @param definition
  * @return
  * @throws PersistenceException
  * @throws SQLException
  * @throws TransactionException
  */
 @SuppressWarnings("deprecation")
 @Override
 public Object beginTransaction(final EntityManager entityManager, final TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException {
  boolean infoEnabled = false ;
  boolean debugEnabled = false ;
  Session session = (Session) entityManager.getDelegate();
  if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
   getSession(entityManager).getTransaction().setTimeout(definition.getTimeout());
  }


  Connection connection = session.connection();
  infoEnabled = logger.isInfoEnabled() ;
  debugEnabled = logger.isDebugEnabled() ;
  if(infoEnabled){
   logger.info("Connection Info: isolationlevel={} , instance={} ", connection.getTransactionIsolation() , connection);
   logger.info("Transaction Info: IsolationLevel={} , PropagationBehavior={} , Timeout={} , Name={}", new Object[]{definition.getIsolationLevel() ,definition.getPropagationBehavior() , definition.getTimeout() , definition.getName()});
  }
  if(debugEnabled){
   logger.debug("The isolation level of the connection is {} and the isolation level set on the transaction is {}", connection.getTransactionIsolation(), definition.getIsolationLevel());
  }
  Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(connection, definition);
  if(infoEnabled){
   logger.info("The previousIsolationLevel {}", previousIsolationLevel);
  }

  entityManager.getTransaction().begin();
  if(infoEnabled){
   logger.debug("Transaction started");
  }

  Object transactionDataFromHibernateJpaTemplate = prepareTransaction(entityManager, definition.isReadOnly(), definition.getName());
  
  return new IsolationSupportSessionTransactionData(transactionDataFromHibernateJpaTemplate, previousIsolationLevel ,connection) ;
 }


 /* (non-Javadoc)
  * @see org.springframework.orm.jpa.vendor.HibernateJpaDialect#cleanupTransaction(java.lang.Object)
  */
 @Override
 public void cleanupTransaction(Object transactionData) {
  super.cleanupTransaction(((IsolationSupportSessionTransactionData) transactionData).getSessionTransactionDataFromHibernateTemplate());
  ((IsolationSupportSessionTransactionData) transactionData).resetIsolationLevel() ;
 }
 
 private static class IsolationSupportSessionTransactionData {

  private final Object sessionTransactionDataFromHibernateJpaTemplate;
  private final Integer previousIsolationLevel;
  private final Connection connection;

  public IsolationSupportSessionTransactionData(Object sessionTransactionDataFromHibernateJpaTemplate, Integer previousIsolationLevel , Connection connection) {
   this.sessionTransactionDataFromHibernateJpaTemplate = sessionTransactionDataFromHibernateJpaTemplate;
   this.previousIsolationLevel = previousIsolationLevel;
   this.connection = connection ;
  }

  public void resetIsolationLevel() {
   if (this.previousIsolationLevel != null) {
    DataSourceUtils.resetConnectionAfterTransaction(connection, previousIsolationLevel) ;
   }
  }
  
  public Object getSessionTransactionDataFromHibernateTemplate(){
   return this.sessionTransactionDataFromHibernateJpaTemplate ;
  }
  
 }

}

Here i would like to highlight the cleanup stuff that I introduced to gracefully restore the connection isolation level that was actually when we received it.

In beginTransaction method before returning store the connection and the previous isolation level in the same way as it is done in the super class i.e. HibernateJpaTemplate the only difference is, it's  a wrapper over HibernateJpaTemplate transaction's data in to our own class that cares for both the hibernate transaction data plus the connection and isolation level.


Finally in cleanupTransaction I simply call the super to do the cleanup stuff required by HibarnateJpaDialect and then reset the connection isolation level.

As we are using the hibernate 3.2.7.ga and doWork(Work work) came in hibernate 3.3 so I have used the session.connection()instead.

Code listing 3: EntityManager integration using spring configuration.

5 comments:

  1. I just hit the same issue however I'm using OpenJPA. Do you think it's possible to adjust the code to use the decorator pattern to implement a JpaDialect decorator so that it works for both Hibernate and OpenJPA (and all other JpaDialect implementations)?.

    I keep track of inventory for Products and each Product has a list of inventory Mutations. The sum of all mutation quantities is the total product quantity. I have a service method with which you can set an absolute value. This requires reading the sum of quantities and calculating a Mutation quantity that would result in the new absolute stock quantity.

    This is a check-then-act which should be atomic. If I understand transaction isolation correctly, this would require Isolation.SERIALIZABLE which would be a bit expensive to use on all transactions.

    I don't understand why this is not supported. I am not the only one with such a requirement on JPA or am I? Or are other programmers not concerned with their data integrity?

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Thanks for the implementation - I am testing it out and found that the resetIsolationLevel() method should call "DataSourceUtils. resetConnectionAfterTransaction(connection, previousIsolationLevel)" even if previousIsolationLevel is null, so as to reset readonly connections.

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete