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

时间:2014-08-15 07:21:59

标签: java spring hibernate jpa

我在我正在处理的其中一个应用程序中使用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但为什么回滚会因异常而崩溃?

5 个答案:

答案 0 :(得分:52)

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

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

  1. 事务拦截器拦截方法调用,并启动事务,因为没有事务已经处于活动状态
  2. 该方法被称为
  3. 该方法调用MyService.doSth()
  4. 事务拦截器拦截方法调用,发现事务已经处于活动状态,并且不执行任何操作
  5. 执行doSth()并抛出异常
  6. 事务拦截器拦截异常,将事务标记为rollbackOnly,并传播异常
  7. ServiceUser.method()捕获异常并返回
  8. 事务拦截器,因为它已启动事务,尝试提交它。但是Hibernate拒绝这样做是因为事务被标记为rollbackOnly,所以Hibernate会抛出异常。事务拦截器通过抛出包装hibernate异常的异常来向调用者发送信号。
  9. 现在,如果ServiceUser.method()不是事务性的,那么会发生以下情况:

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

答案 1 :(得分:16)

  

无法提交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)。

方案:

  1. 标记为由method2抛出的异常进行回滚的事务。这是JB Nizet解释的默认案例。

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

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

  4. 使用method2注释@Transactional(noRollbackFor = SomeException)可防止标记事务以进行回滚(退出method1无异常)。

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

  6. 假设method2未注明@Transactional。从method1调用它确实通过Spring的代理,但Spring不关注抛出的异常。在这种情况下,交易未标记为回滚

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

  8. 如果检查SomeException (不扩展RuntimeException),默认情况下,Spring在拦截已检查的异常时不会标记回滚事务(无异常退出method1时抛出。

  9. 查看this gist中测试的所有方案。

答案 2 :(得分:1)

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

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

在整个代码中添加此代码,可以通过编号调试语句并查看上述方法从“ false”变为“ true”的位置,从而缩小了根本原因。

答案 3 :(得分:0)

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

Transactional.TxType参考文档是here

答案 4 :(得分:0)

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

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