Could not commit JPA transaction: Transaction marked as rollbackOnly
JavaSpringHibernateJpaJava Problem Overview
I'm using Spring and Hibernate in one of the applications that I'm working on and I've got a problem with handling of transactions.
I've got a service class that loads some entities from the database, modifies some of their values and then (when everything is valid) commits these changes to the database. If the new values are invalid (which I can only check after setting them) I do not want to persist the changes. To prevent Spring/Hibernate from saving the changes I throw an exception in the method. This however results in the following error:
Could not commit JPA transaction: Transaction marked as rollbackOnly
And this is the service:
@Service
class MyService {
@Transactional(rollbackFor = MyCustomException.class)
public void doSth() throws MyCustomException {
//load entities from database
//modify some of their values
//check if they are valid
if(invalid) { //if they arent valid, throw an exception
throw new MyCustomException();
}
}
}
And this is how I invoke it:
class ServiceUser {
@Autowired
private MyService myService;
public void method() {
try {
myService.doSth();
} catch (MyCustomException e) {
// ...
}
}
}
What I'd expect to happen: No changes to the database and no exception visible to the user.
What happens: No changes to the database but the app crashes with:
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction;
nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
It's correctly setting the transaction to rollbackOnly but why is the rollback crashing with an exception?
Java Solutions
Solution 1 - Java
My guess is that ServiceUser.method()
is itself transactional. It shouldn't be. Here's the reason why.
Here's what happens when a call is made to your ServiceUser.method()
method:
- the transactional interceptor intercepts the method call, and starts a transaction, because no transaction is already active
- the method is called
- the method calls MyService.doSth()
- the transactional interceptor intercepts the method call, sees that a transaction is already active, and doesn't do anything
- doSth() is executed and throws an exception
- the transactional interceptor intercepts the exception, marks the transaction as rollbackOnly, and propagates the exception
- ServiceUser.method() catches the exception and returns
- the transactional interceptor, since it has started the transaction, tries to commit it. But Hibernate refuses to do it because the transaction is marked as rollbackOnly, so Hibernate throws an exception. The transaction interceptor signals it to the caller by throwing an exception wrapping the hibernate exception.
Now if ServiceUser.method()
is not transactional, here's what happens:
- the method is called
- the method calls MyService.doSth()
- the transactional interceptor intercepts the method call, sees that no transaction is already active, and thus starts a transaction
- doSth() is executed and throws an exception
- the transactional interceptor intercepts the exception. Since it has started the transaction, and since an exception has been thrown, it rollbacks the transaction, and propagates the exception
- ServiceUser.method() catches the exception and returns
Solution 2 - Java
> Could not commit JPA transaction: Transaction marked as rollbackOnly
This exception occurs when you invoke nested methods/services also marked as @Transactional
. JB Nizet explained the mechanism in detail. I'd like to add some scenarios when it happens as well as some ways to avoid it.
Suppose we have two Spring services: Service1
and Service2
. From our program we call Service1.method1()
which in turn calls Service2.method2()
:
class Service1 {
@Transactional
public void method1() {
try {
...
service2.method2();
...
} catch (Exception e) {
...
}
}
}
class Service2 {
@Transactional
public void method2() {
...
throw new SomeException();
...
}
}
SomeException
is unchecked (extends RuntimeException) unless stated otherwise.
Scenarios:
-
Transaction marked for rollback by exception thrown out of
method2
. This is our default case explained by JB Nizet. -
Annotating
method2
as@Transactional(readOnly = true)
still marks transaction for rollback (exception thrown when exiting frommethod1
). -
Annotating both
method1
andmethod2
as@Transactional(readOnly = true)
still marks transaction for rollback (exception thrown when exiting frommethod1
). -
Annotating
method2
with@Transactional(noRollbackFor = SomeException)
prevents marking transaction for rollback (no exception thrown when exiting frommethod1
). -
Suppose
method2
belongs toService1
. Invoking it frommethod1
does not go through Spring's proxy, i.e. Spring is unaware ofSomeException
thrown out ofmethod2
. Transaction is not marked for rollback in this case. -
Suppose
method2
is not annotated with@Transactional
. Invoking it frommethod1
does go through Spring's proxy, but Spring pays no attention to exceptions thrown. Transaction is not marked for rollback in this case. -
Annotating
method2
with@Transactional(propagation = Propagation.REQUIRES_NEW)
makesmethod2
start new transaction. That second transaction is marked for rollback upon exit frommethod2
but original transaction is unaffected in this case (no exception thrown when exiting frommethod1
). -
In case
SomeException
is checked (does not extend RuntimeException), Spring by default does not mark transaction for rollback when intercepting checked exceptions (no exception thrown when exiting frommethod1
).
See all scenarios tested in this gist.
Solution 3 - Java
For those who can't (or don't want to) setup a debugger to track down the original exception which was causing the rollback-flag to get set, you can just add a bunch of debug statements throughout your code to find the lines of code which trigger the rollback-only flag:
logger.debug("Is rollbackOnly: " + TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());
Adding this throughout the code allowed me to narrow down the root cause, by numbering the debug statements and looking to see where the above method goes from returning "false" to "true".
Solution 4 - Java
As explained @Yaroslav Stavnichiy if a service is marked as transactional spring tries to handle transaction itself. If any exception occurs then a rollback operation performed. If in your scenario ServiceUser.method() is not performing any transactional operation you can use @Transactional.TxType annotation. 'NEVER' option is used to manage that method outside transactional context.
Transactional.TxType reference doc is here.
Solution 5 - Java
Save sub object first and then call final repository save method.
@PostMapping("/save")
public String save(@ModelAttribute("shortcode") @Valid Shortcode shortcode, BindingResult result) {
Shortcode existingShortcode = shortcodeService.findByShortcode(shortcode.getShortcode());
if (existingShortcode != null) {
result.rejectValue(shortcode.getShortcode(), "This shortode is already created.");
}
if (result.hasErrors()) {
return "redirect:/shortcode/create";
}
**shortcode.setUser(userService.findByUsername(shortcode.getUser().getUsername()));**
shortcodeService.save(shortcode);
return "redirect:/shortcode/create?success";
}