Java 无法提交 JPA 事务:事务标记为 rollbackOnly
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/25322658/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Could not commit JPA transaction: Transaction marked as rollbackOnly
提问by user3346601
I'm using Spring and Hibernate in one of the applications that I'm working on and I've got a problem with handling of transactions.
我在我正在处理的应用程序之一中使用 Spring 和 Hibernate,但在处理事务时遇到了问题。
I've got a service class that loads some entities from the database, modifies some of their values and then (when everything is valid) commits these changes to the database. If the new values are invalid (which I can only check after setting them) I do not want to persist the changes. To prevent Spring/Hibernate from saving the changes I throw an exception in the method. This however results in the following error:
我有一个服务类,它从数据库加载一些实体,修改它们的一些值,然后(当一切都有效时)将这些更改提交到数据库。如果新值无效(我只能在设置后检查),我不想保留更改。为了防止 Spring/Hibernate 保存更改,我在方法中抛出了一个异常。然而,这会导致以下错误:
Could not commit JPA transaction: Transaction marked as rollbackOnly
And this is the service:
这是服务:
@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();
}
}
}
And this is how I invoke it:
这就是我调用它的方式:
class ServiceUser {
@Autowired
private MyService myService;
public void method() {
try {
myService.doSth();
} catch (MyCustomException e) {
// ...
}
}
}
What I'd expect to happen: No changes to the database and no exception visible to the user.
我希望发生的事情:数据库没有更改,用户也没有看到异常。
What happens: No changes to the database but the app crashes with:
发生了什么:数据库没有变化,但应用程序崩溃了:
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction;
nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
It's correctly setting the transaction to rollbackOnly but why is the rollback crashing with an exception?
它正确地将事务设置为 rollbackOnly 但为什么回滚会因异常而崩溃?
采纳答案by JB Nizet
My guess is that ServiceUser.method()
is itself transactional. It shouldn't be. Here's the reason why.
我的猜测是这ServiceUser.method()
本身就是事务性的。不应该。这就是原因。
Here's what happens when a call is made to your ServiceUser.method()
method:
以下是调用您的ServiceUser.method()
方法时发生的情况:
- the transactional interceptor intercepts the method call, and starts a transaction, because no transaction is already active
- the method is called
- the method calls MyService.doSth()
- the transactional interceptor intercepts the method call, sees that a transaction is already active, and doesn't do anything
- doSth() is executed and throws an exception
- the transactional interceptor intercepts the exception, marks the transaction as rollbackOnly, and propagates the exception
- ServiceUser.method() catches the exception and returns
- the transactional interceptor, since it has started the transaction, tries to commit it. But Hibernate refuses to do it because the transaction is marked as rollbackOnly, so Hibernate throws an exception. The transaction interceptor signals it to the caller by throwing an exception wrapping the hibernate exception.
- 事务拦截器拦截方法调用,并启动一个事务,因为没有事务已经处于活动状态
- 该方法被称为
- 该方法调用 MyService.doSth()
- 事务拦截器拦截方法调用,看到一个事务已经处于活动状态,不做任何事情
- doSth() 被执行并抛出异常
- 事务拦截器拦截异常,将事务标记为rollbackOnly,并传播异常
- ServiceUser.method() 捕获异常并返回
- 事务拦截器,因为它已经启动了事务,尝试提交它。但是Hibernate拒绝这样做,因为事务被标记为rollbackOnly,所以Hibernate抛出异常。事务拦截器通过抛出一个包含休眠异常的异常来向调用者发出信号。
Now if ServiceUser.method()
is not transactional, here's what happens:
现在,如果ServiceUser.method()
不是事务性的,则会发生以下情况:
- the method is called
- the method calls MyService.doSth()
- the transactional interceptor intercepts the method call, sees that no transaction is already active, and thus starts a transaction
- doSth() is executed and throws an exception
- the transactional interceptor intercepts the exception. Since it has started the transaction, and since an exception has been thrown, it rollbacks the transaction, and propagates the exception
- ServiceUser.method() catches the exception and returns
- 该方法被称为
- 该方法调用 MyService.doSth()
- 事务拦截器拦截方法调用,看到没有事务已经处于活动状态,从而启动一个事务
- doSth() 被执行并抛出异常
- 事务拦截器拦截异常。由于它已经启动了事务,并且由于抛出了异常,它回滚事务,并传播异常
- ServiceUser.method() 捕获异常并返回
回答by Yaroslav Stavnichiy
Could not commit JPA transaction: Transaction marked as rollbackOnly
无法提交 JPA 事务:事务标记为 rollbackOnly
This exception occurs when you invoke nested methods/services also marked as @Transactional
. JB Nizet explained the mechanism in detail. I'd like to add some scenarioswhen it happens as well as some ways to avoid it.
当您调用也标记为 的嵌套方法/服务时,会发生此异常@Transactional
。JB Nizet 详细解释了该机制。我想在它发生时添加一些场景以及一些避免它的方法。
Suppose we have two Spring services: Service1
and Service2
. From our program we call Service1.method1()
which in turn calls Service2.method2()
:
假设我们有两个 Spring 服务:Service1
和Service2
。在我们的程序中,我们调用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
is unchecked (extends RuntimeException) unless stated otherwise.
SomeException
除非另有说明,否则未选中(扩展 RuntimeException)。
Scenarios:
场景:
Transaction marked for rollback by exception thrown out of
method2
. This is our default case explained by JB Nizet.Annotating
method2
as@Transactional(readOnly = true)
still marks transaction for rollback (exception thrown when exiting frommethod1
).Annotating both
method1
andmethod2
as@Transactional(readOnly = true)
still marks transaction for rollback (exception thrown when exiting frommethod1
).Annotating
method2
with@Transactional(noRollbackFor = SomeException)
prevents marking transaction for rollback (no exceptionthrown when exiting frommethod1
).Suppose
method2
belongs toService1
. Invoking it frommethod1
does not go through Spring's proxy, i.e. Spring is unaware ofSomeException
thrown out ofmethod2
. Transaction is not marked for rollbackin this case.Suppose
method2
is not annotated with@Transactional
. Invoking it frommethod1
does go through Spring's proxy, but Spring pays no attention to exceptions thrown. Transaction is not marked for rollbackin this case.Annotating
method2
with@Transactional(propagation = Propagation.REQUIRES_NEW)
makesmethod2
start new transaction. That second transaction is marked for rollback upon exit frommethod2
but original transaction is unaffected in this case (no exceptionthrown when exiting frommethod1
).In case
SomeException
is checked(does not extend RuntimeException), Spring by default does not mark transaction for rollback when intercepting checked exceptions (no exceptionthrown when exiting frommethod1
).
事务被从 中抛出的异常标记为回滚
method2
。这是 JB Nizet 解释的默认情况。注释
method2
为@Transactional(readOnly = true)
仍然标记回滚事务(从 退出时抛出异常method1
)。注释
method1
和method2
as@Transactional(readOnly = true)
仍然标记回滚事务(从 退出时抛出异常method1
)。注释
method2
with@Transactional(noRollbackFor = SomeException)
可防止将事务标记为回滚(退出时不会抛出异常method1
)。假设
method2
属于Service1
. 调用它 frommethod1
不经过 Spring 的代理,即 Spring 不知道SomeException
抛出method2
. 在这种情况下,事务没有标记为回滚。假设
method2
没有用 注释@Transactional
。调用它 frommethod1
确实要通过 Spring 的代理,但 Spring 并不关心抛出的异常。在这种情况下,事务没有标记为回滚。标注
method2
有@Transactional(propagation = Propagation.REQUIRES_NEW)
品牌method2
开始新的事务。第二个事务在退出时被标记为回滚,method2
但在这种情况下原始事务不受影响(退出时不会抛出异常method1
)。如果
SomeException
是checked(不扩展RuntimeException),Spring在拦截checked异常时默认不标记事务回滚(退出时不抛出异常method1
)。
See all scenarios tested in this gist.
查看本要点中测试的所有场景。
回答by mahkras
As explained @Yaroslav Stavnichiy if a service is marked as transactional spring tries to handle transaction itself. If any exception occurs then a rollback operation performed. If in your scenario ServiceUser.method() is not performing any transactional operation you can use @Transactional.TxType annotation. 'NEVER' option is used to manage that method outside transactional context.
正如@Yaroslav Stavnichiy 所解释的,如果服务被标记为事务性 spring 会尝试自行处理事务。如果发生任何异常,则执行回滚操作。如果在您的场景中 ServiceUser.method() 没有执行任何事务操作,您可以使用 @Transactional.TxType 注释。'NEVER' 选项用于在事务上下文之外管理该方法。
Transactional.TxType reference doc is here.
Transactional.TxType 参考文档在这里。
回答by Nirbhay Rana
Save sub object first and then call final repository save method.
首先保存子对象,然后调用最终存储库保存方法。
@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";
}
回答by Joel Richard Koett
For those who can't (or don't want to) setup a debugger to track down the original exception which was causing the rollback-flag to get set, you can just add a bunch of debug statements throughout your code to find the lines of code which trigger the rollback-only flag:
对于那些不能(或不想)设置调试器来跟踪导致回滚标志设置的原始异常的人,您可以在整个代码中添加一堆调试语句来查找行触发仅回滚标志的代码:
logger.debug("Is rollbackOnly: " + TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());
Adding this throughout the code allowed me to narrow down the root cause, by numbering the debug statements and looking to see where the above method goes from returning "false" to "true".
在整个代码中添加这个允许我缩小根本原因,通过对调试语句进行编号并查看上述方法从返回“false”到“true”的位置。