Spring JPA更新异常:无法获取实体及唯一约束未拦截后续执行
问题分析与解决方案
问题1原因
deviceDao.save()操作的是Hibernate持久化上下文(Session)中的实体对象,修改后保存会同步更新上下文内的缓存。后续findById查询时,Hibernate优先从上下文缓存读取数据,因此能拿到更新后的实体。- 自定义的
updateAsset()方法使用@Modifying注解的JPQL语句,属于直接执行数据库SQL,完全绕过了Hibernate的持久化上下文。此时上下文内的Device实体仍保持旧状态,后续查询会直接读取缓存里的旧数据,无法获取更新后的值。默认情况下@Modifying不会自动清除上下文缓存,必须手动触发刷新或配置clearAutomatically = true。
问题2原因
Spring事务默认的异常回滚规则是:仅当事务方法内抛出**未检查异常(RuntimeException及其子类)**时才触发回滚,但这里的约束违例异常存在延迟抛出的情况:
- 使用
@Modifying的JPQL更新操作,Hibernate默认会将DML语句缓存,直到事务提交、执行查询操作触发flush,或者手动调用flush时才会真正执行SQL。 - 当
updateClass.doSth()执行完更新后,SQL并未立即执行,事务继续走到messagePublisher.doSth()。此时findById查询触发Hibernate flush缓存中的DML操作,这才抛出ConstraintViolationException。但此时messagePublisher的代码已经执行完毕(因为查询先从上下文缓存拿到了修改后的实体),最终事务因异常回滚,但消息已经发送。
问题2的替代解决办法(除提前查询外)
- 手动触发Flush:在
updateAsset()方法执行后,立即调用deviceDao.flush(),强制Hibernate执行SQL,让约束违例异常在updateClass.doSth()阶段就抛出,终止后续代码执行:// UpdateClass.doRealThing中修改 deviceDao.updateAsset(assetId, deviceId); deviceDao.flush(); // 新增手动flush - 配置@Modifying自动清除缓存:在DAO的
updateAsset()方法上添加clearAutomatically = true,确保上下文缓存与数据库同步:@Modifying(clearAutomatically = true) @Query("UPDATE device a SET a.asset = ?1 WHERE a.device = ?2") int updateAsset(UUID asset, UUID device); - 捕获异常并终止流程:在
updateClass.doRealThing()中捕获DataIntegrityViolationException(Spring对Hibernate约束违例的包装类),抛出异常中断后续执行:protected void doRealThing(UUID deviceId, UUID assetId) { Optional<Device> device = deviceDao.findById(deviceId); if (device.isPresent()) { try { deviceDao.updateAsset(assetId, deviceId); deviceDao.flush(); } catch (DataIntegrityViolationException e) { throw new RuntimeException("资产ID重复,更新失败", e); } } } - 使用EntityManager手动控制:注入
EntityManager,执行更新后调用entityManager.flush()和entityManager.clear(),强制同步数据库状态并清除上下文缓存。
相关代码
ExampleDeviceDao
public interface ExampleDeviceDao extends JpaRepository<Device, UUID>, JpaSpecificationExecutor<Device> { @Modifying @Query("UPDATE device a SET a.asset = ?1 WHERE a.device = ?2") int updateAsset(UUID asset, UUID device); }
公共服务类Service
@Component public class Service { @Autowired UpdateClass updateClass; @Autowired MiddleClass middleClass; @Autowired MessagePublisher messagePublisher; @org.springframework.transaction.annotation.Transactional public void updateAsset(UUID deviceId, UUID assetId) { updateClass.doSth(deviceId, assetId); middleClass.doSth(deviceId, assetId); messagePublisher.doSth(deviceId, assetId); } }
AbstractClass
public abstract class AbstractClass { protected abstract void doRealThing(UUID deviceId, UUID assetId); public void doSth(UUID deviceId, UUID assetId) { doRealThing(deviceId, assetId); } }
UpdateClass
@Component public class UpdateClass extends AbstractClass{ @Autowired ExampleDeviceDao deviceDao; protected void doRealThing(UUID deviceId, UUID assetId) { Optional<Device> device = deviceDao.findById(deviceId); if (device.isPresent()) { device.get().setAsset(assetId); /** approach 1:*/ deviceDao.save(device.get()); /** * approach 2: * deviceDao.updateAsset(assetId, deviceId); */ } } }
MiddleClass
@Component public class MiddleClass extends AbstractClass{ protected void doRealThing(UUID deviceId, UUID assetId) { //do other things, not db update or query. } }
MessagePublisher
@Component public class MessagePublisher extends AbstractClass{ @Autowired ExampleDeviceDao deviceDao; @Autowired SNSMessagePublisher snsMessagePublisher; protected void doRealThing(UUID deviceId, UUID assetId) { Optional<Device> device = deviceDao.findById(deviceId); if (device.isPresent()) { snsMessagePublisher.publishMessage(device.get()); } } }
内容的提问来源于stack exchange,提问作者Tiee
相关产品推荐
相关产品推荐

