55

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?

user3346601
  • 1,019
  • 1
  • 11
  • 18
  • 1
    My guess is that ServiceUser.method() is itself transactional. It shouldn't be. Post the full stack trace. – JB Nizet Aug 15 '14 at 07:26
  • try using `noRollbackFor` instead of `rollbackFor` in the `@Transactional` annotation – papacito Aug 15 '14 at 07:30
  • @JBNizet Wow good guess, there was actually an `@Transactional` annotation at ServiceUser's class declaration. After removing that it works perfectly. Could you please post your comment as an answer (and maybe give a short explanation of the reason why it shouldnt be transactional)? – user3346601 Aug 15 '14 at 07:41

6 Answers6

80

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:

  1. the transactional interceptor intercepts the method call, and starts a transaction, because no transaction is already active
  2. the method is called
  3. the method calls MyService.doSth()
  4. the transactional interceptor intercepts the method call, sees that a transaction is already active, and doesn't do anything
  5. doSth() is executed and throws an exception
  6. the transactional interceptor intercepts the exception, marks the transaction as rollbackOnly, and propagates the exception
  7. ServiceUser.method() catches the exception and returns
  8. 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:

  1. the method is called
  2. the method calls MyService.doSth()
  3. the transactional interceptor intercepts the method call, sees that no transaction is already active, and thus starts a transaction
  4. doSth() is executed and throws an exception
  5. 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
  6. ServiceUser.method() catches the exception and returns
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Thanks for your explanation. One question though: What would I do if I needed a transaction inside `ServiceUser.method()`? For example if I wanted to access lazily-loaded associations of entities? – user3346601 Aug 15 '14 at 07:57
  • 4
    You could probably use REQUIRES_NEW for the doSth() method. The doSth method would then execute in its own transaction which could be rollbacked without affecting the method() transaction. – JB Nizet Aug 15 '14 at 08:02
  • Thanks again! Setting `propagation = Propagation.REQUIRES_NEW` on `doSth` works! – user3346601 Aug 15 '14 at 08:15
  • 1
    I am not able to get point 6,8. If Transactional Interceptor knows there is rollback why does it tries to commit ? – balboa_21 Mar 31 '17 at 16:23
  • @balboa_21 good question. " If Transactional Interceptor knows there is rollback why does it tries to commit ? " point 6. – Blocked Jan 27 '21 at 19:17
30

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:

  1. Transaction marked for rollback by exception thrown out of method2. This is our default case explained by JB Nizet.

  2. Annotating method2 as @Transactional(readOnly = true) still marks transaction for rollback (exception thrown when exiting from method1).

  3. Annotating both method1 and method2 as @Transactional(readOnly = true) still marks transaction for rollback (exception thrown when exiting from method1).

  4. Annotating method2 with @Transactional(noRollbackFor = SomeException) prevents marking transaction for rollback (no exception thrown when exiting from method1).

  5. 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.

  6. 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.

  7. 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).

  8. 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.

Yaroslav Stavnichiy
  • 20,738
  • 6
  • 52
  • 55
  • Great info. Right now your point 7 came to my help but in my case suppose `Service1.method1().dao1()` is happening and I want **if Service2.method2() fails(or do a rollback) then`Service1.method1().dao1()` should also do a rollback**? – balboa_21 Mar 30 '17 at 18:38
  • @balboa_21In case you do want to roll back, just let exception from inner method fly through outer Transactional method as well (or re-throw). – Yaroslav Stavnichiy Mar 31 '17 at 13:31
  • but if services are catching exceptions ? . Am I correct here "if one transaction is active then in case of another service with `@Transaction` will reuse it" ?. So wouldn't there be any way of making transaction failure cascading – balboa_21 Mar 31 '17 at 16:10
  • 1
    @balboa_21 Transaction reuse depends on `propagation` setting. E.g. `REQUIRES_NEW` does not reuse transaction therefore even when inner transaction is marked for rollback, outer transaction is not affected. Throwing exception is the preferred way of marking transaction for rollback, although you can use transaction manager API directly, eg. see http://stackoverflow.com/a/34933457/697313 – Yaroslav Stavnichiy Apr 01 '17 at 13:46
  • Thanks I was able to solve my issued after 3 days debugging – Obot Ernest Nov 04 '22 at 12:08
2

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".

1

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.

mahkras
  • 541
  • 4
  • 6
1

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";
    }
Nirbhay Rana
  • 4,229
  • 2
  • 18
  • 4
0

For me this happened due to constraint violation , when I was trying to update not null field with null value using save.

Dhiraj Surve
  • 310
  • 2
  • 5