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

Posted

技术标签:

【中文标题】无法提交 JPA 事务:事务标记为 rollbackOnly【英文标题】:Could not commit JPA transaction: Transaction marked as rollbackOnly 【发布时间】:2014-10-08 23:12: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

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

【问题讨论】:

我的猜测是 ServiceUser.method() 本身就是事务性的。不应该。发布完整的堆栈跟踪。 尝试在@Transactional注解中使用noRollbackFor而不是rollbackFor @JBNizet 哇,好猜,在 ServiceUser 的类声明中实际上有一个 @Transactional 注释。删除它后,它可以完美运行。您能否发表您的评论作为答案(或许可以简要解释一下为什么它不应该是事务性的)? 【参考方案1】:

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

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

    事务拦截器拦截方法调用,并启动事务,因为没有事务处于活动状态 方法被调用 方法调用 MyService.doSth() 事务拦截器拦截方法调用,发现事务已经处于活动状态,不做任何事情 doSth() 被执行并抛出异常 事务拦截器拦截异常,将事务标记为rollbackOnly,并传播异常 ServiceUser.method() 捕获异常并返回 事务拦截器,因为它已经启动了事务,尝试提交它。但是Hibernate拒绝这样做,因为事务被标记为rollbackOnly,所以Hibernate抛出了异常。事务拦截器通过抛出一个包装休眠异常的异常向调用者发出信号。

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

    方法被调用 方法调用 MyService.doSth() 事务拦截器拦截方法调用,发现没有事务处于活动状态,因此启动事务 doSth() 被执行并抛出异常 事务拦截器拦截异常。由于它已经启动了事务,并且由于抛出了异常,所以它回滚事务,并传播异常 ServiceUser.method() 捕获异常并返回

【讨论】:

感谢您的解释。不过有一个问题:如果我需要在ServiceUser.method() 中进行交易,我会怎么做?例如,如果我想访问延迟加载的实体关联? 你可以使用 REQUIRES_NEW 来做 doSth() 方法。然后 doSth 方法将在其自己的事务中执行,该事务可以回滚而不影响 method() 事务。 再次感谢!在doSth 上设置propagation = Propagation.REQUIRES_NEW 有效! 我无法获得第 6,8 点。如果 Transactional Interceptor 知道有回滚,为什么它会尝试提交? @balboa_21 好问题。 “如果事务拦截器知道有回滚,为什么它会尝试提交?”第 6 点。【参考方案2】:

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

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

假设我们有两个 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();
        ...
    

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

场景:

    由于method2 抛出的异常而标记为回滚的事务。这是 JB Nizet 解释的默认情况。

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

    method1method2 注释为@Transactional(readOnly = true) 仍将事务标记为回滚(从method1 退出时抛出异常)。

    使用@Transactional(noRollbackFor = SomeException) 注释method2 可防止将事务标记为回滚(从method1 退出时抛出无异常)。

    假设method2 属于Service1。从 method1 调用它不会通过 Spring 的代理,即 Spring 不知道从 method2 抛出的 SomeException。在这种情况下,事务未标记为回滚

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

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

    如果SomeException选中(不扩展RuntimeException),Spring在拦截检查异常时默认不会将事务标记为回滚(无异常method1 退出时抛出)。

查看this gist 中测试的所有场景。

【讨论】:

很棒的信息。现在你的第 7 点对我有帮助,但在我的情况下,假设 Service1.method1().dao1() 正在发生,我希望 如果 Service2.method2() 失败(或执行回滚)然后Service1.method1().dao1() 也应该执行回滚? @balboa_21如果您确实想回滚,只需让内部方法的异常也通过外部事务方法(或重新抛出)。 但是如果服务捕获异常? .我在这里是否正确“如果一个事务处于活动状态,那么在另一个具有@Transaction 的服务的情况下将重用它”?那么不会有任何方式使交易失败级联 @balboa_21 事务重用取决于propagation 设置。例如。 REQUIRES_NEW 不重用事务,因此即使内部事务标记为回滚,外部事务也不受影响。抛出异常是标记事务以进行回滚的首选方式,尽管您可以直接使用事务管理器 API,例如。见***.com/a/34933457/697313【参考方案3】:

对于那些不能(或不想)设置调试器来跟踪导致设置回滚标志的原始异常的人,您可以在整个代码中添加一堆调试语句找到触发仅回滚标志的代码行:

logger.debug("Is rollbackOnly: " + TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());

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

【讨论】:

【参考方案4】:

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

Transactional.TxType 参考文档是here。

【讨论】:

【参考方案5】:

先保存子对象,然后调用最终存储库保存方法。

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

【讨论】:

以上是关于无法提交 JPA 事务:事务标记为 rollbackOnly的主要内容,如果未能解决你的问题,请参考以下文章

Spring Data JPA + Hibernate 将方法标记为事务

jpa @postpersist @postupdate 仅在事务提交后

JPA 事务未提交给数据库

Spring JPA 存储库事务性

JPA 事务未在方法结束时提交

org.springframework.transaction.CannotCreateTransactionException:无法为事务打开 JPA EntityManager