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

56
我正在使用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状态,但为什么回滚操作会引发异常呢?


1
我的猜测是ServiceUser.method()本身就是事务性的。但它不应该是这样的。请发布完整的堆栈跟踪。 - JB Nizet
@Transactional注释中,尝试使用noRollbackFor而不是rollbackFor - papacito
@JBNizet 好猜测,ServiceUser类声明中实际上有一个@Transactional注释。删除后它完美地工作了。您能否将您的评论发布为答案(并可能简要解释为什么不应该是事务性的)? - user3346601
6个回答

81

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

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

  1. 事务性拦截器拦截方法调用,并启动一个事务,因为没有活动中的事务
  2. 调用该方法
  3. 该方法调用MyService.doSth()
  4. 事务性拦截器拦截方法调用,看到已经有活动中的事务,所以不做任何操作
  5. 执行doSth() 并抛出异常
  6. 事务性拦截器拦截异常,将事务标记为rollbackOnly,并传播异常
  7. ServiceUser.method()捕获异常并返回
  8. 事务性拦截器自己启动了事务,试图提交事务。但是由于事务被标记为rollbackOnly,因此Hibernate拒绝提交它,因此Hibernate抛出异常。事务拦截器通过抛出包装了Hibernate异常的异常向调用者发出信号。

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

  1. 调用该方法
  2. 该方法调用MyService.doSth()
  3. 事务性拦截器拦截方法调用,发现没有活动中的事务,因此启动一个事务
  4. 执行doSth() 并抛出异常
  5. 事务性拦截器拦截异常。由于它已经启动了事务,并且已经抛出异常,因此它回滚了事务并传播了异常
  6. ServiceUser.method()捕获异常并返回

谢谢您的解释。不过我有一个问题:如果我需要在 ServiceUser.method() 中使用事务怎么办?例如,如果我想要访问实体的延迟加载关联。 - user3346601
4
您可以在doSth()方法中使用REQUIRES_NEW。这样,doSth方法将在其自己的事务中执行,该事务可以进行回滚而不影响method()事务。 - JB Nizet
再次感谢!在 doSth 上设置 propagation = Propagation.REQUIRES_NEW 就可以了! - user3346601
1
我无法理解第6点和第8点。如果事务拦截器知道需要回滚,为什么还要尝试提交呢? - balboa_21
@balboa_21 很好的问题。第6点:“如果事务拦截器知道需要回滚,为什么还要尝试提交?” - Blocked

30

无法提交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不知道从method2抛出的SomeException。 在这种情况下,事务不会标记为回滚

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

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

  8. 如果SomeException已检查的异常(不是RuntimeException的子类),则Spring默认情况下拦截已检查异常时不会标记事务回滚(从method1退出时没有异常被抛出)。

请参阅此代码片段中测试的所有场景。


非常有用的信息。现在你所提到的第七点对我很有帮助,但是在我的情况下,如果 Service1.method1().dao1() 正在发生,并且我想要如果 Service2.method2() 失败(或回滚),那么Service1.method1().dao1() 也应该回滚,怎么办? - balboa_21
如果您确实想要回滚,请让内部方法的异常通过外部事务方法传递(或重新抛出)。 - Yaroslav Stavnichiy
但如果服务正在捕获异常呢?我这里的理解是,“如果一个事务处于活动状态,那么在另一个带有@Transaction注释的服务中将重用它”,我的理解正确吗?那么难道没有任何方法可以使事务失败级联吗? - balboa_21
1
@balboa_21 事务重用取决于“传播”设置。例如,“REQUIRES_NEW”不会重用事务,因此即使内部事务标记为回滚,外部事务也不受影响。抛出异常是将事务标记为回滚的首选方法,尽管您也可以直接使用事务管理器API,例如请参见https://dev59.com/wGsz5IYBdhLWcg3weHuU#34933457。 - Yaroslav Stavnichiy
谢谢,经过三天的调试,我终于解决了我的问题。 - Obot Ernest

2

对于那些无法(或不想)设置调试器以跟踪导致回滚标志被设置的原始异常的人,您可以在代码中添加大量调试语句来查找触发仅回滚标志的代码行:

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

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

1

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

Transactional.TxType参考文档在这里


1
先保存子对象,然后调用最终存储库的保存方法。
@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";
    }

0

对我来说,这是由于约束违规引起的,当我尝试使用save更新非空字段为null值时发生了这种情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接