Could not commit JPA transaction: Transaction marked as rollbackOnly
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
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.
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".
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.