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?
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 from method1).
Annotating both method1 and method2 as #Transactional(readOnly = true) still marks transaction for rollback (exception thrown when exiting from method1).
Annotating method2 with #Transactional(noRollbackFor = SomeException) prevents marking transaction for rollback (no exception thrown when exiting from method1).
Suppose method2 belongs to Service1. Invoking it from method1 does not go through Spring's proxy, i.e. Spring is unaware of SomeException thrown out of method2. Transaction is not marked for rollback in this case.
Suppose method2 is not annotated with #Transactional. Invoking it from method1 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) makes method2 start new transaction. That second transaction is marked for rollback upon exit from method2 but original transaction is unaffected in this case (no exception thrown when exiting from method1).
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 from method1).
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.
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";
}
Related
I'm working with a DB2 database and tested following code:
no matter methodB has Propagation.REQUIRES_NEW or not, if methodB has exception, methodA's result will be committed correctly regardless.
This is against my assumption that Propagation.REQUIRES_NEW must be used to achieve this.
ClassA
#Autowire
private ClassB classB;
#Transactional
methodA(){
...
try{
classB.methodB();
}catch(RuntimeException ex){
handleException(ex);
}
...
}
ClassB
#Transactional(propagation = Propagation.REQUIRES_NEW)
methodB(){...}
Thanks for #Kayaman I think I figured it out now.
The behaviour I saw is because methodB's #Transactional annotation didn't work, so methodB is treated as a normal function without any transaction annotation.
Where it went wrong is that in methodA, I called methodB from a subclasss of ClassB by super.methodB() and thought that it will give a transactional methodB, which isn't working:
#Service
#Primary
ClassC extends ClassB{
#override
methodB(){
super.methodB();
}
}
I know that transaction annotation will not work if you call a transaction method from another non-transactional method of the same class.
Didn't know that super.methodB() will also fail for the same reason (anyone can give a bit more explanation pls?)
In conclusion, in the example of the first block of code, when methodB has RuntimeException,
If methodB has NO transaction annotation: A & B share the same transaction; methodA will NOT rollback
if methodB has REQUIRED annotation: A & B share the same transaction; methodA will rollback
if methodB has REQUIRES_NEW annotation: A & B have separate transactions; methodA will NOT rollback
Without REQUIRES_NEW (i.e. the default REQUIRED or one of the others that behaves in a similar way), ClassB.methodB() participates in the same transaction as ClassA.methodA(). An exception in in methodB() will mark that same transaction to be rolled back. Even if you catch the exception, the transaction will be rolled back.
With REQUIRES_NEW, the transaction rolled back will be particular to methodB(), so when you catch the exception, there's still the healthy original non-rolled back transaction in existence.
ClassA
#Transactional
methodA(){
try{
classB.methodB();
}catch(RuntimeException ex){
handleException(ex);
}
}
ClassB
#Transactional
methodB(){
throw new RuntimeException();
}
The above code will rollback the whole transaction. With propagation=TransactionPropagation.REQUIRES_NEW for methodB() it will not.
Without any annotation for methodB(), there will be only one tx boundary at methodA() level and Spring will not be aware that an exception is thrown, since it's caught in the middle of the method. This is similar to inlining the contents of methodB() to methodA(). If that exception is a non-database exception (e.g. NullPointerException), the transaction will commit normally. If that exception is a database exception, the underlying database transaction is set to be rolled back, but Spring isn't aware of that. Spring then tries to commit, and throws an UnexpectedRollbackException, because the database won't allow the tx to be committed.
Leaving the annotation out explicitly or accidentally is wrong. If you intend to perform db operations you must be working with a well defined transaction context, and know your propagation.
Calling super.methodB() bypasses Spring's normal proxying mechanism, so even though there is an annotation, it's ignored. Finally, calling super.methodB() seems like a design smell to me. Using inheritance to cut down on lines is often bad practice, and in this case caused a serious bug.
It's not exactly as the title says, but close to. Consider these Spring beans:
#Bean
class BeanA {
#Transactional(propagation = Propagation.REQUIRED, rollbackFor = EvilException.class)
public void methodA() {
/* ... some actions */
if (condition) {
throw new EvilException();
}
}
}
#Bean
class BeanB {
#Autowired private BeanA beanA;
final int MAX_TRIES = 3;
#Transactional(propagation = Propagation.NESTED)
public void methodB() {
// prepare to call Bean A
try {
beanA.methodA();
/* maybe do some more things */
}
catch (EvilException e) {
/* recover from evil */
}
}
}
#Bean
class MainWorkerBean {
#Autowired private BeanB beanB;
#Autowired private OtherBean otherBean;
#Transactional(propagation = Propagation.REQUIRED)
public void doSomeWork() {
beanB.methodB();
otherBean.doSomeWork();
}
}
Important note: I'm using JDBC transaction manager that supports savepoints.
What I'm expecting this to do is, when EvilException is thrown, the transaction of the BeanA is rolled back, which with this setup happens to be the savepoint created by starting methodB. However, this appears to not be the case.
When going over with debugging tools, what I'm seeing is this:
When doSomeWork of MainWorkerBean starts, new transaction is created
When methodB starts, transaction manager properly initializes a savepoint and hands it to TransactionInterceptor
When methodA starts, transaction manager sees Propagation.REQUIRED again, and hands out a clean reference to the actual JDBC transaction again, that has no knowledge of the savepoint
This means that when exception is thrown, TransactionStatus::hasSavepoint return false, which leads to roll back of the whole global transaction, so recovery and further steps are as good as lost, but my actual code has no knowledge of the rollback (since I've written recovery for it).
For now, I can't consider changing BeanA's transaction to Propagation.NESTED. Admittedly, looks like it's going to allow me to have the more local rollback, but it's going to be too local, because as I understand it, Spring then will have two savepoints, and only roll back the BeanA savepoint, not BeanB one, as I'd like.
Is there anything else I'm missing, such as a configuration option, that would make internal transaction with Propagation.REQUIRED consider that it is running inside a savepoint, and roll back to savepoint, not the whole thing?
Right now we're using Spring 4.3.24, but I already crawled through their code and can't spot any relevant changes, so I don't think upgrading will help me.
As described in this bug ticket: https://github.com/spring-projects/spring-framework/issues/11234
For spring versions < 5.0, in the situation described, the global transaction is set to 'rollback-only'.
In this transaction I am processing several tasks. If an error should occur during a single task, I do not want the whole transaction to be rolled back, therefore I wrap the task processing in another transaction boundary with a propagation of PROPAGATION_NESTED.
The problem comes when, during task processing, calls are made to existing service methods defined with a transaction boundary of PROPAGATION_REQUIRED. Any runtime exceptions thrown from these methods cause the underlying connection to be marked as rollback-only, rather than respecting the current parent transaction nested propagation.
[...]
As of Spring Framework 5.0, nested transactions resolve their rollback-only status on a rollback to a savepoint, not applying it to the global transaction anymore.
On older versions, the recommended workaround is to switch globalRollbackOnParticipationFailure to false in such scenarios.
However, even for Spring5, I noticed when reproducing the problem, that the nested transaction may be rolled back, including all things done in the catch block of methodB(). So your recover code might not work inside methodB(), depending on what your recovery looks like. If methodA() was not transactional, that would not happen. Just something to watch out for.
Some more details to be found here: https://github.com/spring-projects/spring-framework/issues/8135
Currently we are facing a spring transaction related issue in our application.
As you can see that in deleteRecord() we are doing a DB operation. But in the next line
a business exception is thrown.
Expected Behavior(As per my knowledge) :
The DB operation should be rolled back as exception is thrown from the next line
Actual Behavior :
Its not getting rolled back. Data is getting inserted to the table
Question :
Why transaction is not getting rolled back ? I dont think its because of the catch block
because deleteRecord() will be executed in new transaction. Please correct me if I am wrong
Code:
class A {
void myMethod() {
for(int i=0 ; i<count ; i++) {
try {
deleteRecord();
} catch(Exception e) {
log.error("Exception caught");
}
}
}
#Transactional(propagation = Propagation.REQUIRES_NEW)
deleteRecord() throws Exception{
line 1 : deleting record
line 2 : Throwing business exception
}
}
The Spring documentation says the following:
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.
And
In its default configuration, the Spring Frameworkâs transaction
infrastructure code only marks a transaction for rollback in the case
of runtime, unchecked exceptions; that is, when the thrown exception
is an instance or subclass of RuntimeException. ( Errors will also -
by default - result in a rollback). Checked exceptions that are thrown
from a transactional method do not result in rollback in the default
configuration
see in 16.5.3: https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html
This says that the default behavior of the transaction will only rollback for RuntimeExceptions. If you have a own business exception (could be a checked excpetion), you have to explicitly name the exception class the transaction should rollback for:
#Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = YOUREXCEPTION.class)
change to
#Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
It is because of #Transaction method call by the method within the same class, does not work if you do not configured Spring to use AspectJ.
Spring #Transaction method call by the method within the same class, does not work?
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?
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 from method1).
Annotating both method1 and method2 as #Transactional(readOnly = true) still marks transaction for rollback (exception thrown when exiting from method1).
Annotating method2 with #Transactional(noRollbackFor = SomeException) prevents marking transaction for rollback (no exception thrown when exiting from method1).
Suppose method2 belongs to Service1. Invoking it from method1 does not go through Spring's proxy, i.e. Spring is unaware of SomeException thrown out of method2. Transaction is not marked for rollback in this case.
Suppose method2 is not annotated with #Transactional. Invoking it from method1 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) makes method2 start new transaction. That second transaction is marked for rollback upon exit from method2 but original transaction is unaffected in this case (no exception thrown when exiting from method1).
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 from method1).
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.
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";
}
#Transactional (noRollbackFor=RuntimeException.class)
public void methodA (Entity e){
service.methodB(e);
}
---service method below---
#Transactional (propagation=Propagation.REQUIRES_NEW, noRollbackFor=RuntimeException.class)
public void methodB (Entity e){
dao.insert(e);
}
When dao.insert(e) in methodB() causes a primary key violation and throws a ConstraintViolationException, which is a subclass of RuntimeException, I would expect the transaction to still commit because of the noRollbackFor property I used. But I observed that the outer transaction (on methodA) is still being rolled back by the HibernateTransactionManager with the message
org.springframework.transaction.UnexpectedRollback Exception:
Transaction rolled back because it has been marked as rollback-only
I've found similar questions reported but not exactly this one.
Once an exception is caught, the Hibernate Session should be discarded and the transaction should be rolled back:
If the Session throws an exception, the transaction must be rolled
back and the session discarded. The internal state of the Session
might not be consistent with the database after the exception occurs.
So, noRollbackFor applies to your Service and DAO layer that might throw an exception. Let's say you have a gatewayService that write to a Database through a Hibernate DAO and also sends an email through an emailService. If the emailService throws a SendMailFailureException you can instruct the gatewayService not to roll back when it will catch this exception:
#Transactional(noRollbackFor=SendMailFailureException.class)
public void saveAndSend(Entity e){
dao.save(e);
emailService.send(new Email(e));
}