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

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

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

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) { // ... } } } 

我期望发生的事情:没有对数据库的更改,也没有对用户可见的exception。

会发生什么:没有更改数据库但应用程序崩溃:

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

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

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

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

  1. 事务拦截器拦截方法调用,并启动事务,因为没有事务已经处于活动状态
  2. 该方法被调用
  3. 该方法调用MyService.doSth()
  4. 事务拦截器拦截方法调用,发现事务已经处于活动状态,并且不执行任何操作
  5. doSth()被执行并抛出exception
  6. 事务拦截器拦截exception,将事务标记为rollbackOnly,并传播exception
  7. ServiceUser.method()捕获exception并返回
  8. 事务拦截器,因为它已启动事务,尝试提交它。 但是Hibernate拒绝这样做是因为事务被标记为rollbackOnly,所以Hibernate会抛出exception。 事务拦截器通过抛出包装hibernateexception的exception将其发送给调用者。

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

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

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

当您调用也标记为@Transactional嵌套方法/服务时,会发生此exception 。 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. 标记为由方法2抛出的exception进行回滚的事务。 这是JB Nizet解释的默认情况。

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

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

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

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

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

  7. 使用@Transactional(propagation = Propagation.REQUIRES_NEW)注释method2使方法2启动新事务。 第二个事务在从方法2退出时标记为回滚,但在这种情况下原始事务不受影响(退出method1不会抛出exception)。

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

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

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

Transactional.TxType参考文档在这里 。

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

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

对于那些不能(或不想)设置调试器来追踪导致rollback-flag设置的原始exception的人,你可以在整个代码中添加一堆调试语句来查找这些行触发回滚标志的代码:

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

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