我在我正在处理的其中一个应用程序中使用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但为什么回滚会因异常而崩溃?
答案 0 :(得分:52)
我的猜测是ServiceUser.method()
本身就是交易性的。它不应该。这就是原因。
以下是调用ServiceUser.method()
方法时发生的情况:
现在,如果ServiceUser.method()
不是事务性的,那么会发生以下情况:
答案 1 :(得分:16)
无法提交JPA事务:事务标记为rollbackOnly
当您调用也标记为@Transactional
的嵌套方法/服务时,会发生此异常。 JB Nizet详细解释了这种机制。我想在发生时添加一些场景以及一些避免它的方法。
假设我们有两个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
未经检查(扩展RuntimeException)。
方案:
标记为由method2
抛出的异常进行回滚的事务。这是JB Nizet解释的默认案例。
将method2
注释为@Transactional(readOnly = true)
仍标记回滚事务(退出method1
时抛出异常)。
将method1
和method2
注释为@Transactional(readOnly = true)
仍标记回滚事务(退出method1
时抛出异常)。
使用method2
注释@Transactional(noRollbackFor = SomeException)
可防止标记事务以进行回滚(退出method1
时无异常)。
假设method2
属于Service1
。从method1
调用它不会通过Spring的代理,即Spring不知道SomeException
抛出method2
。在这种情况下,交易未标记为回滚。
假设method2
未注明@Transactional
。从method1
调用它确实通过Spring的代理,但Spring不关注抛出的异常。在这种情况下,交易未标记为回滚。
使用method2
注释@Transactional(propagation = Propagation.REQUIRES_NEW)
会使method2
开始新的交易。第二个事务在退出method2
时标记为回滚,但原始事务在这种情况下不受影响(退出method1
时无异常)。
如果检查SomeException
(不扩展RuntimeException),默认情况下,Spring在拦截已检查的异常时不会标记回滚事务(无异常退出method1
时抛出。
查看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";
}