Could not commit JPA transaction: Transaction marked as rollbackOnly

前端 未结 5 1576
不知归路
不知归路 2020-11-30 21:45

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

相关标签:
5条回答
  • 2020-11-30 22:20

    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.

    0 讨论(0)
  • 2020-11-30 22:27

    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";
        }
    
    0 讨论(0)
  • 2020-11-30 22:33

    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.

    0 讨论(0)
  • 2020-11-30 22:39

    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
    0 讨论(0)
  • 2020-11-30 22:45

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

    0 讨论(0)
提交回复
热议问题