Spring存储含已有关联实体的新实体时抛出PersistentObjectException
这个异常的核心原因很明确:你通过deviceSystemPersistor.findByName()获取的DeviceSystem实例处于游离(detached)状态——简单说就是它已经脱离了当前的Hibernate持久化上下文,而你的Device实体中system字段配置了CascadeType.PERSIST,当调用devicePersistor.save(device)时,Hibernate会尝试对关联的DeviceSystem执行持久化操作,但游离实体不能直接被persist,所以抛出了这个错误。
下面给你几种可行的解决思路,根据你的业务场景选最合适的:
1. 移除不需要的级联类型(最直接的方案)
既然你明确不想新建DeviceSystem,那完全不需要给system字段配置CascadeType.PERSIST。修改Device实体中的关联注解:
@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.MERGE) // 如果连级联更新都不需要,直接去掉cascade参数即可: // @OneToOne(fetch = FetchType.EAGER) @JoinColumn(name = "device_system_id", referencedColumnName = "device_system_id") @NotNull @Valid private DeviceSystem system;
这样Hibernate在保存Device时,只会处理Device本身的持久化,不会尝试去持久化关联的DeviceSystem,自然就不会触发异常。
2. 将游离的DeviceSystem重新附着到当前持久化上下文
如果你需要保留级联配置(比如后续可能需要级联更新DeviceSystem),那可以在保存Device前,把游离的DeviceSystem重新变成托管(managed)状态:
方案A:在Controller中重新获取托管实例
给Controller方法加上@Transactional确保在同一个事务上下文里,然后通过ID重新查询DeviceSystem:
@PostMapping @Transactional public void saveDevice(@RequestBody Device device) { // 通过ID重新获取处于托管状态的DeviceSystem DeviceSystem attachedSystem = deviceSystemPersistor.findById(device.getSystem().getId()) .orElseThrow(() -> new IllegalArgumentException("Invalid DeviceSystem name")); device.setSystem(attachedSystem); devicePersistor.save(device); }
这里findById返回的实例是当前事务上下文托管的,关联到Device后保存就不会有问题。
方案B:在Deserializer中使用EntityManager.merge()
如果想在反序列化阶段就处理好,可以注入EntityManager并调用merge()方法,把游离实例转换为托管实例:
public class DeviceDeserializer extends JsonDeserializer<Device> { @Autowired private DeviceSystemPersistor deviceSystemPersistor; @PersistenceContext private EntityManager entityManager; @Override public Device deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { JsonNode deviceNode = jsonParser.readValueAsTree(); String appVersion = deviceNode.get("appVersion").asText(); DeviceSystem deviceSystem = deviceSystemPersistor.findByName(deviceNode.get("system").asText()); // 将游离实例转换为托管实例 deviceSystem = entityManager.merge(deviceSystem); String systemVersion = deviceNode.get("systemVersion").asText(); String model = deviceNode.get("model").asText(); return new Device(appVersion, deviceSystem, systemVersion, model); } }
注意要确保Deserializer的操作处于事务范围内,否则merge()可能无法正常工作。
3. 使用轻量代理获取托管实例
如果不需要加载DeviceSystem的所有字段,只是需要关联它,可以用EntityManager.getReference()获取一个托管的代理对象(不会触发实际的数据库查询,除非访问非ID字段):
// 先通过name查询到对应的ID(需要给DeviceSystemPersistor新增一个查询ID的方法) Long systemId = deviceSystemPersistor.findIdByName(deviceNode.get("system").asText()); // 获取托管的代理实例 DeviceSystem deviceSystem = entityManager.getReference(DeviceSystem.class, systemId);
这种方式性能更好,也能避免游离实体的问题。
额外注意点
- 确保你的数据操作在事务中:如果Controller方法没有
@Transactional,devicePersistor.save()会开启一个新的事务上下文,此时传入的DeviceSystem已经是游离状态,就会触发异常。 - 检查
DeviceSystemPersistor的查询方法:如果查询操作不在事务中,查询完成后持久化上下文关闭,返回的实体就会变成游离状态。
内容的提问来源于stack exchange,提问作者smeeb

