无法提交 JPA 事务:标记为仅回滚的事务

2022-08-31 20:09:44

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

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

Could not commit JPA transaction: Transaction marked as rollbackOnly

这是服务:

@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();
    }

  }
}

这就是我如何调用它:

class ServiceUser {
  @Autowired
  private MyService myService;

  public void method() {
    try {
      myService.doSth();
    } catch (MyCustomException e) {
      // ...
    }        
  }
}

我期望发生的事情:不对数据库进行任何更改,也没有对用户可见的异常。

发生的情况:未对数据库进行任何更改,但应用崩溃:

org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction;
nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly

它正确地将事务设置为仅回滚,但为什么回滚崩溃并出现异常?


答案 1

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

以下是调用方法时发生的情况:ServiceUser.method()

  1. 事务拦截器截获方法调用,并启动事务,因为没有事务处于活动状态
  2. 该方法称为
  3. 该方法调用 MyService.doSth()
  4. 事务拦截器拦截方法调用,查看事务已处于活动状态,并且不执行任何操作
  5. doSth() 被执行并引发异常
  6. 事务拦截器截获异常,将事务标记为仅回滚,并传播异常
  7. ServiceUser.method() 捕获异常并返回
  8. 事务拦截器,因为它已经启动了事务,尝试提交它。但是Hibernate拒绝这样做,因为事务被标记为rollbackOnly,所以Hibernate会引发异常。事务侦听器通过引发包装休眠异常的异常向调用方发出信号。

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

  1. 该方法称为
  2. 该方法调用 MyService.doSth()
  3. 事务拦截器拦截方法调用,看到没有事务已处于活动状态,从而启动事务
  4. doSth() 被执行并引发异常
  5. 事务侦听器截获异常。由于它已启动事务,并且由于已引发异常,因此它将回滚事务并传播异常
  6. ServiceUser.method() 捕获异常并返回

答案 2

无法提交 JPA 事务:标记为仅回滚的事务

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

假设我们有两个 Spring 服务:和 .从我们的程序中,我们称之为,反过来又调用:Service1Service2Service1.method1()Service2.method2()

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

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

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

场景:

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

  2. 批注为仍标记要回滚的事务(退出 时引发异常)。method2@Transactional(readOnly = true)method1

  3. 对两者都进行批注 仍会将事务标记为回滚(退出 时引发异常)。method1method2@Transactional(readOnly = true)method1

  4. 对 进行批注可防止将事务标记为回滚(退出 时不会引发异常)。method2@Transactional(noRollbackFor = SomeException)method1

  5. 假设属于 。从 中调用它不会通过 Spring 的代理,即 Spring 不知道是否被扔出 。在这种情况下,事务不会标记为回滚method2Service1method1SomeExceptionmethod2

  6. 假设未用 注释。从中调用它确实会通过Spring的代理,但Spring不会注意抛出的异常。在这种情况下,事务不会标记为回滚method2@Transactionalmethod1

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

  8. 如果已选中(不扩展 RuntimeException),则默认情况下,Spring 在拦截已检查的异常时不会将事务标记为回滚(退出时不会引发异常)。SomeExceptionmethod1

查看此要点中测试的所有方案。


推荐