Java 无法提交 JPA 事务:事务标记为 rollbackOnly

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/25322658/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 00:06:06  来源:igfitidea点击:

Could not commit JPA transaction: Transaction marked as rollbackOnly

javaspringhibernatejpa

提问by user3346601

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.

我在我正在处理的应用程序之一中使用 Spring 和 Hibernate,但在处理事务时遇到了问题。

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:

我有一个服务类,它从数据库加载一些实体,修改它们的一些值,然后(当一切都有效时)将这些更改提交到数据库。如果新值无效(我只能在设置后检查),我不想保留更改。为了防止 Spring/Hibernate 保存更改,我在方法中抛出了一个异常。然而,这会导致以下错误:

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?

它正确地将事务设置为 rollbackOnly 但为什么回滚会因异常而崩溃?

采纳答案by JB Nizet

My guess is that ServiceUser.method()is itself transactional. It shouldn't be. Here's the reason why.

我的猜测是这ServiceUser.method()本身就是事务性的。不应该。这就是原因。

Here's what happens when a call is made to your ServiceUser.method()method:

以下是调用您的ServiceUser.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.
  1. 事务拦截器拦截方法调用,并启动一个事务,因为没有事务已经处于活动状态
  2. 该方法被称为
  3. 该方法调用 MyService.doSth()
  4. 事务拦截器拦截方法调用,看到一个事务已经处于活动状态,不做任何事情
  5. doSth() 被执行并抛出异常
  6. 事务拦截器拦截异常,将事务标记为rollbackOnly,并传播异常
  7. ServiceUser.method() 捕获异常并返回
  8. 事务拦截器,因为它已经启动了事务,尝试提交它。但是Hibernate拒绝这样做,因为事务被标记为rollbackOnly,所以Hibernate抛出异常。事务拦截器通过抛出一个包含休眠异常的异常来向调用者发出信号。

Now if ServiceUser.method()is not transactional, here's what happens:

现在,如果ServiceUser.method()不是事务性的,则会发生以下情况:

  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
  1. 该方法被称为
  2. 该方法调用 MyService.doSth()
  3. 事务拦截器拦截方法调用,看到没有事务已经处于活动状态,从而启动一个事务
  4. doSth() 被执行并抛出异常
  5. 事务拦截器拦截异常。由于它已经启动了事务,并且由于抛出了异常,它回滚事务,并传播异常
  6. ServiceUser.method() 捕获异常并返回

回答by Yaroslav Stavnichiy

Could not commit JPA transaction: Transaction marked as rollbackOnly

无法提交 JPA 事务:事务标记为 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 scenarioswhen it happens as well as some ways to avoid it.

当您调用也标记为 的嵌套方法/服务时,会发生此异常@Transactional。JB Nizet 详细解释了该机制。我想在它发生时添加一些场景以及一些避免它的方法

Suppose we have two Spring services: Service1and Service2. From our program we call Service1.method1()which in turn calls Service2.method2():

假设我们有两个 Spring 服务:Service1Service2。在我们的程序中,我们调用Service1.method1()它依次调用Service2.method2()

class Service1 {
    @Transactional
    public void method1() {
        try {
            ...
            service2.method2();
            ...
        } catch (Exception e) {
            ...
        }
    }
}

class Service2 {
    @Transactional
    public void method2() {
        ...
        throw new SomeException();
        ...
    }
}

SomeExceptionis unchecked (extends RuntimeException) unless stated otherwise.

SomeException除非另有说明,否则未选中(扩展 RuntimeException)。

Scenarios:

场景:

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

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

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

  4. Annotating method2with @Transactional(noRollbackFor = SomeException)prevents marking transaction for rollback (no exceptionthrown when exiting from method1).

  5. Suppose method2belongs to Service1. Invoking it from method1does not go through Spring's proxy, i.e. Spring is unaware of SomeExceptionthrown out of method2. Transaction is not marked for rollbackin this case.

  6. Suppose method2is not annotated with @Transactional. Invoking it from method1does go through Spring's proxy, but Spring pays no attention to exceptions thrown. Transaction is not marked for rollbackin this case.

  7. Annotating method2with @Transactional(propagation = Propagation.REQUIRES_NEW)makes method2start new transaction. That second transaction is marked for rollback upon exit from method2but original transaction is unaffected in this case (no exceptionthrown when exiting from method1).

  8. In case SomeExceptionis checked(does not extend RuntimeException), Spring by default does not mark transaction for rollback when intercepting checked exceptions (no exceptionthrown when exiting from method1).

  1. 事务被从 中抛出的异常标记为回滚method2。这是 JB Nizet 解释的默认情况。

  2. 注释method2@Transactional(readOnly = true)仍然标记回滚事务(从 退出时抛出异常method1)。

  3. 注释method1method2as@Transactional(readOnly = true)仍然标记回滚事务(从 退出时抛出异常method1)。

  4. 注释method2with@Transactional(noRollbackFor = SomeException)可防止将事务标记为回滚(退出时不会抛出异常method1)。

  5. 假设method2属于Service1. 调用它 frommethod1不经过 Spring 的代理,即 Spring 不知道SomeException抛出method2. 在这种情况下,事务没有标记为回滚

  6. 假设method2没有用 注释@Transactional。调用它 frommethod1确实要通过 Spring 的代理,但 Spring 并不关心抛出的异常。在这种情况下,事务没有标记为回滚

  7. 标注method2@Transactional(propagation = Propagation.REQUIRES_NEW)品牌method2开始新的事务。第二个事务在退出时被标记为回滚,method2但在这种情况下原始事务不受影响(退出时不会抛出异常method1)。

  8. 如果SomeExceptionchecked(不扩展RuntimeException),Spring在拦截checked异常时默认不标记事务回滚(退出时抛出异常method1)。

See all scenarios tested in this gist.

查看本要点中测试的所有场景。

回答by mahkras

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.

正如@Yaroslav Stavnichiy 所解释的,如果服务被标记为事务性 spring 会尝试自行处理事务。如果发生任何异常,则执行回滚操作。如果在您的场景中 ServiceUser.method() 没有执行任何事务操作,您可以使用 @Transactional.TxType 注释。'NEVER' 选项用于在事务上下文之外管理该方法。

Transactional.TxType reference doc is here.

Transactional.TxType 参考文档在这里

回答by Nirbhay Rana

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";
    }

回答by Joel Richard Koett

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

在整个代码中添加这个允许我缩小根本原因,通过对调试语句进行编号并查看上述方法从返回“false”到“true”的位置。