A Transaction Gotcha When Using Spring with Hibernate

Spring’s transaction propagation rule SUPPORTS states that if there exists a transaction when the method is called, it should work within that transaction, otherwise it should work outside any transaction. Similarly, transaction propagation rule NOT_SUPPORTED states that if there exists any active transaction when the method is called, that active transaction should be suspended before method is run, and method should run outside any active transaction.

However, things work a bit odd when Spring transaction mechanism is used with Hibernate as persistence technology at the backend. First of all we should know that although transaction propagation is set as SUPPORTS or NOT_SUPPORTED, Spring still creates a logical transaction so that it can execute if there exists any registered TransactionSynchronization objects at the end of the method call. Another thing to know that, we also need an active physical Hibernate Transaction object if we want to use contextual session feature of Hibernate in our DAO objects, and actually problem starts here. If you use contextual session of Hibernate within your DAO objects by calling sessionFactory.getCurrentSession(), SpringSessionContext which is in charge of implementing contextual session capability within Spring environment, registers a SpringSessionSynchronization object when getCurrentSession() is called and switches Hibernate Session flush mode to AUTO if it is MANUAL unless transaction is marked as readOnly=true.

When method completes with success, TransactionInterceptor performs commit! Yes, what you read here is correct! Spring transaction support performs a commit whenever method returns with success even no actual transaction exists. Although HibernateTransactionManager which is actually in change of managing transactions, won’t commit as there is no actual transaction, it still executes registered TransactionSynchronization objects consecutively. SpringSessionSynchroization, at this point invokes Hibernate Session flush, and our SQL DML statements are sent to DB. Unfortunately, Hibernate physical transaction commits when Session is closed, even though transaction.commit() is not invoked explicitly. Hence, our changes become permanent in the DB.

This scenario is valid both for SUPPORTS and NOT_SUPPORTED propagation rules. One workaround is to mark transaction as readOnly=true. This causes Spring to set Hibernate Session flush mode to MANUAL, and no flush occurs during SpringSessionSchronization.beforeCommit() method call. The other one is to change transactionSynchronization behaviour of Spring’s PlatformTransactionManager to ON_ACTUAL_TRANSACTION. If you change it so, Spring won’t register any TransactionSynchronization object including SpringSessionSynchronization. In that case, you won’t be able to use contextual session capability within your DAOs. Therefore, it is better to go with the first option, and DON’T forget to mark your transactions as readOnly=true if their propagation rule is either SUPPORTS or NOT_SUPPORTED on your service methods.