无法提交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,但为什么回滚会异常崩溃?

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

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

  1. 事务拦截器拦截方法调用,并启动一个事务,因为没有事务已经是活动的
  2. 方法被称为
  3. 方法调用MyService.doSth()
  4. 事务拦截器拦截方法调用,看到事务已经激活,不做任何事情
  5. doSth()被执行并抛出异常
  6. 事务拦截器拦截异常,将事务标记为rollbackOnly,并传播异常
  7. ServiceUser.method()捕获异常并返回
  8. 事务拦截器,因为它已经启动了事务,尝试提交它。但是Hibernate拒绝这样做,因为事务被标记为rollbackOnly,因此Hibernate抛出一个异常。事务拦截器通过抛出包含hibernate异常的异常向调用者发出信号。

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

  1. 方法被称为
  2. 方法调用MyService.doSth()
  3. 事务拦截器拦截方法调用,看到没有事务已经激活,因此启动一个事务
  4. doSth()被执行并抛出异常
  5. 事务拦截器拦截异常。因为它已经启动了事务,并且抛出了异常,所以它回滚事务,并传播异常
  6. ServiceUser.method()捕获异常并返回

无法提交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. @Transactional(noRollbackFor = SomeException)注释method2可以防止标记事务回滚(没有异常退出method1时抛出)。

  5. method2属于Service1。从method1调用它不经过Spring的代理,即Spring不知道SomeExceptionmethod2抛出。事务未标记为回滚

  6. 假设method2未标注@Transactional。从method1调用它确实通过Spring的代理,但是Spring不注意抛出的异常。事务未标记为回滚

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

  8. 如果SomeException检查(不扩展RuntimeException), Spring默认不标记事务回滚时拦截检查异常(没有异常抛出时退出method1)。

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

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

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

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

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

事务。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";
    }

对我来说,这是由于约束违反,当我试图更新非空字段使用空值保存

最新更新