我正在开发的一个应用程序中使用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()
方法时发生的情况:
- 事务拦截器拦截方法调用,并启动一个事务,因为没有事务已经是活动的
- 方法被称为
- 方法调用MyService.doSth()
- 事务拦截器拦截方法调用,看到事务已经激活,不做任何事情 doSth()被执行并抛出异常
- 事务拦截器拦截异常,将事务标记为rollbackOnly,并传播异常 ServiceUser.method()捕获异常并返回
- 事务拦截器,因为它已经启动了事务,尝试提交它。但是Hibernate拒绝这样做,因为事务被标记为rollbackOnly,因此Hibernate抛出一个异常。事务拦截器通过抛出包含hibernate异常的异常向调用者发出信号。
现在,如果ServiceUser.method()
不是事务性的,会发生以下情况:
- 方法被称为
- 方法调用MyService.doSth()
- 事务拦截器拦截方法调用,看到没有事务已经激活,因此启动一个事务 doSth()被执行并抛出异常
- 事务拦截器拦截异常。因为它已经启动了事务,并且抛出了异常,所以它回滚事务,并传播异常 ServiceUser.method()捕获异常并返回
无法提交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
时抛出异常)。用
@Transactional(noRollbackFor = SomeException)
注释method2
可以防止标记事务回滚(没有异常退出method1
时抛出)。设
method2
属于Service1
。从method1
调用它不经过Spring的代理,即Spring不知道SomeException
从method2
抛出。事务未标记为回滚。假设
method2
未标注@Transactional
。从method1
调用它确实通过Spring的代理,但是Spring不注意抛出的异常。事务未标记为回滚。用
@Transactional(propagation = Propagation.REQUIRES_NEW)
标注method2
使method2
开始新的事务。第二个事务在退出method2
时被标记为回滚,但在这种情况下原始事务不受影响(退出method1
时没有抛出异常)。如果
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";
}
对我来说,这是由于约束违反,当我试图更新非空字段使用空值保存