A Word on Spring @Transactional and Exceptions
Spring's @Transactional is very popular among developers. Yet, many of them are unaware of its weird exception handling convention, which can lead to inconsistent data.
Join the DZone community and get the full member experience.
Join For FreeA unit of work transaction either succeeds or fails and rolls back. So suppose we are dealing with transferring money from one account to another. Now, say money from one account is already debited but when it gets credited to another account, there was an exception. Obviously, the ideal scenario would be for the debited action to roll back. Otherwise, we're dealing with an inconsistent state (and some very angry account owners).
So if we use transactions in our business logic, we can ensure the logic under the transaction works as a unit and that it will roll back if anything is found to be wrong.
Either the unit of work completely succeeds or completely fails. There is no intermediate state. Now the question comes, "How we can handle a transaction?"
There are two ways to handle it:
BMT: Bean Managed Transaction
CMT: Container Managed Transaction
BMT
If we need a finer control over business logic or want to introduce savepoints, this type of technique should be adopted, where a bean provider has a responsibility to start, commit, and roll back the transaction.
CMT
If we want to delegate the responsibility to a container, we use this instead. Sometimes we call it a declarative transaction. We all know in Spring that, using the @Transactional annotation, we can adopt a declarative transaction technique.
A Weird Case
We're not going to go into configuration in this article. Instead, I'm going to warn you about some precautions you want to take while using @Transactional. Study the pseudo code below:
@Transactional
public void transferMoney(Account to, Account from, int amount) throws Exception {
debiFromAccount(from, amount)
creditToAccount(to, amount);
}
public debitFromAccount(Account from, int amount) {
//do staff and debited money from data base
}
public creditToAccount(Account to, int amount) throws Exception {
//do straff
throw new Exception("Error during credit");
}
Now do a dry run:
If Account from's initial balance is 1000
Account to is 500
Transfer amount is 100
Afterward, an error occurred in creditToAccount(Account to,int amount)
What will be the output you expect?
To: 500
From: 1000
After all, I use @Transactional. If there's an exception, it should be rolled back, right?
But unfortunately, it left an inconsistent state.
Our output is:
To: 500
From: 900
What's going on here?
Let me clear this up for you. @Transactional only rolls back transactions for unchecked exceptions. For checked exceptions and their subclasses, it commits data. So although an exception is raised here, because it's a checked exception, Spring ignores it and commits the data to the database, making the system inconsistent.
The Spring documentation says:
While the EJB default behavior is for the EJB container to automatically roll back the transaction on a system exception (usually a runtime exception), EJB CMT does not roll back the transaction automatically on an application exception (that is, a checked exception other than java.rmi.RemoteException). While the Spring default behavior for declarative transaction management follows EJB convention (roll back is automatic only on unchecked exceptions), it is often useful to customize this.
Pay attention to the last line: “it is often useful to customize this.” So how can we customize it?
It's very simple, just use the following with @Transactional:
@Transactional(rollbackFor = Exception.class)
So if you throw an Exception or a subclass of it, always use the above with the @Transactional annotation to tell Spring to roll back transactions if a checked exception occurs.
Published at DZone with permission of Shamik Mitra, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments