A Case Study: Dancing With the @Transactional Annotation in Exceptional Cases
A case study on using the @Transactional annotation in your Java code and how it can both help, and potentially hinder, your development process.
Join the DZone community and get the full member experience.
Join For FreeSpring's @Transactional
annotation is easy to use, but it may cause different types of errors if you don't know about the common pitfalls to look out for. In this article, I will show you the possible errors you can run into.
Suppose we write pseudocode like this: We have a service called DanceService.java and this service has a transactional method called letsDance
. This code runs perfectly. What happens when we have an exception?
Suppose the letsDance
method is called by a Controller or by a web service and proxy mode is used. So the proxy of the DanceService
class handles all the details of the transaction such as opening the connection, committing the transaction, and closing the connection.
According to the pseudocode above, what happens when we get an error in the adjustMusic
method? We want the business logic to continue running after receiving the exception. Inside this method, we call another service’s method: the adjustVolume
method and, in some cases, we throw an exception. How does the remaining code act when the exception is thrown by the adjustVolume
method? Remember the adjustMusic
method of DanceService
class and that we want to ignore all errors in the adjustVolume
method. Ok, let me show you the errors you will have. After the exception, the oneStepLeft
method runs and makes some updates in the database.
xxxxxxxxxx
public void oneStepLeft (){
querySomeDataFromDb();
xxDao.save();
}
But in the first line of code in the oneStepLeft
method, which queries the database or updates the database, the line gets an error: “org.springframework.orm.jpa.JpaSystemException: could not inspect JDBC autocommit mode; nested exception is org.hibernate.exception.GenericJDBCException: could not inspect JDBC autocommit mode”. The pseudocode above has this error when calling queryDataFromDb
.
So why do we have an error? Didn’t they tell us that everything would be fine when we use the @Transactional
annotation?
When we carefully inspect the application server logs, we the following line of code before “could not inspect JDBC autocommit mode” error:
x
Caused by: java.sql.SQLException: Unexpected exception while enlisting XAConnection java.sql.SQLException: Transaction rolled back: setRollbackOnly called on transaction
at weblogic.jdbc.jta.DataSource.enlist(DataSource.java:1795)
at weblogic.jdbc.jta.DataSource.refreshXAConnAndEnlist(DataSource.java:1680)
at weblogic.jdbc.wrapper.JTAConnection.getXAConn(JTAConnection.java:229)
at weblogic.jdbc.wrapper.JTAConnection.checkConnection(JTAConnection.java:91)
at weblogic.jdbc.wrapper.JTAConnection.checkConnection(JTAConnection.java:74)
.....
The transaction rolls back in the catch block of the adjustMusic
method when we get an exception, but it lets the code continue to run. When the first time code wants to access the database for doing some work, the code explodes.
And if we go further into the letsDance
method, we are now in the catch block. Then we jump to the finally
block and call the createDanceLog
method, which is transactional as well.
xxxxxxxxxx
public void createDeviceInfoLog(DanceData danceData){
//…
xxDao.save();
}
What happens when the line of code with save()
runs? Do we succeed or do we have an error?
Unfortunately, we get another error: “org.springframework.dao.InvalidDataAccessApiUsageException: no transaction is in progress; nested exception is javax.persistence.TransactionRequiredException: no transaction is in progress.”
(I love this type of error very much. When I was new to JPA and got this error, I could not solve the problem and fix the error. There is not enough information and solution recommendations on the web.)
Here is the stacktrace:
xxxxxxxxxx
< com.sun.xml.ws.server.sei.TieHandler> <BEA-000000> <no transaction is in progress; nested exception is javax.persistence.TransactionRequiredException: no transaction is in progress
org.springframework.dao.InvalidDataAccessApiUsageException: no transaction is in progress; nested exception is javax.persistence.TransactionRequiredException: no transaction is in progress
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:413)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:436)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213)
Truncated. see log file for complete stacktrace
Caused By: javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.ejb.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:993)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
Truncated. see log file for complete stacktrace
Now, let me tell you the details about the source of this error: the transaction rolls back in the adjustMusic
method and then we continue. Because the transaction is broken down and ends before running the createDeviceInfoLog
method, the transaction is already marked for death. So when we try to get a connection for the database in the method and there is no transaction left, we get a “no transaction is in progress” error.
In this case, what should we do to fix the problem? After receiving an exception from MusicService
in adjustVolume
, the remaining the lines of code should run without getting another exception. When we get an exception from MusicService
, the transaction is rolled back.
Spring will automatically rollback the transaction if a runtime exception is thrown from a transactional method call.
To prevent automatic rollbacks, we should use the noRollbackFor
attribute of the @Transactional
annotation:
@Transactional (noRollbackFor=Exception.class)
This annotation indicates that a rollback should not be issued if the target method raises this exception.
xxxxxxxxxx
noRollbackFor=Exception.class) (
public void adjustVolume(DanceData danceData)
When we update the code above like this and then run the code again, there are no more “setRollbackOnly called on transaction” or “no transaction is in progress” exceptions, because the transaction is not rolled back and it commits perfectly in the end. The code as runs smoothly as we want.
This is a really weird case. When you get “no transaction is in progress error,” you probably say “oh my god, what did I do wrong?” We can solve our problem with the noRollbackFor
attribute, but before using this, we have tried many different code tricks and we have wasted our time and efforts. The solution is inside the details of Spring Declarative Transaction Management.
The @Transaction
annotation is a utility of Spring Declarative Transaction Management and it hides the complexity of dealing with transaction handling codes. However, it is not as easy as just putting the @Transactional
annotation in your code. You should understand the transaction management concept well before starting to code. Besides, you should carefully test your code for exceptional cases.
To sum up, you should be aware of the unexpected steps of your dance partner and you should adjust yourself quickly.
Opinions expressed by DZone contributors are their own.
Comments