使用Hibernate中的瞬态实体来更新/合并现有的持久化对象

8
我正在处理一个相当复杂的对象图表,存储在我的数据库中。我使用XStream来序列化和反序列化这个对象图表,这很有效。当我导入在数据库中已存在的对象图表时,它最初是短暂的,因为没有ID,并且hibernate对其一无所知。然后,我的业务逻辑会确定哪些新短暂导入对象地图中的对象与现有持久对象相对应,并设置相应的ID。然后,我使用Hibernate的merge()和saveOrUpdate()方法。
以下是伪代码,以帮助您更好地理解我的操作:
ComplexObject transObj = xstream.import("object.xml");
ComplexObject persistObj = someService.getObjByName(transObj.getName());
for (OtherObject o : c.getObjects()) {
    if (persistObj.getObjects().contains(o.getName())) {
        o.setId(persistObj.getObjectByName(o.getName()).getId())
    }
    ... set a bunch of other IDs deeper in the object graph ...
}

transObj = session.merge(transObj);
session.saveOrUpdate(transObj);

现在这不起作用了,因为我会得到诸如以下错误:

   org.springframework.dao.InvalidDataAccessApiUsageException: deleted object would be re-saved by cascade (remove deleted object from associations): [com.......SomeObject#353296]; nested exception is org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): [com.......SomeObject#353296]

看起来Hibernate的merge并不适用于将瞬态对象与持久对象关联。

有没有什么方法可以实现我想做的事情,而不必在会话中获取持久对象,修改它,而是修改暂态对象,并尝试保存它并覆盖现有的持久对象?


1
这就是我会解决它的方式:https://dev59.com/nlPTa4cB1Zd3GeqPlKnP -- 为了性能原因,你应该缓存反射/内省数据或从你的实体创建一个类生成器。 - user1050755
拥有一个 setId 总是一个坏主意吗? - Rob
user1050755,这个解决方案的唯一问题是似乎无法处理复杂的级联、一对多等情况。我想这只适用于非图形类型的结构,如果我错了,请纠正我。 - eipark
你能不能换个方式做呢?我的意思是先从Hibernate加载对象,然后再使用XStream编写自定义的marshaller? - mindas
这正是我试图避免的。那可能是可行的,但随后每当数据模型发生变化时,它就会变成维护噩梦,并且由于数据的类似图形的特性,这将是困难的。 - eipark
如果 A 中的一个对您有用,您会接受它吗?Q 仍然是开放的。 - Glen Best
2个回答

8
似乎Hibernate合并操作不适用于将瞬态对象与持久对象关联。Merge是JPA标准,用于将“新的和分离的实体实例”合并到“(持久性上下文)管理的实体实例”中。将在标记为Cascade.MERGE或Cascade.ALL的FK关系级联。从JPA的角度来看,合并操作并不意味着将Xstream流式传输的瞬态对象与持久对象关联。JPA旨在使合并操作与常规JPA生命周期一起工作:创建新对象,将其持久化,获取/查找它们,分离它们,修改它们(包括添加新对象),合并它们,可选地再次修改它们,然后将它们持久化/保存。这是有意设计的,以使JPA流畅且高效-每个单独的对象持久化到数据库都不需要在之前检索对象状态,以确定是否/如何插入/更新。JPA持久性上下文对象状态已经包含足够的细节来确定这一点。您的问题在于您有新的实体实例,您希望它们像已分离一样运行-而这不是JPA的方式。SaveOrUpdate是Hibernate的专有操作-如果实体具有标识,则会更新它,但如果没有实体,则会插入它。从Hibernate的角度来看,合并操作后跟SaveOrUpdate理论上可以用于将(Xstream流式传输的)瞬态对象与持久对象关联,但在与JPA操作一起使用时可能会有限制。SaveOrUpdate在每个对象持久化到数据库之前都会先检索以确定是否/如何插入/更新-它很聪明,但不是JPA,也不是最高效的。即使发生冲突,您应该能够通过谨慎和正确的配置以及在发生冲突时使用Hibernate操作而不是JPA操作来使其正常工作。我遇到了这样的错误:org.hibernate.ObjectDeletedException:删除的对象将通过级联重新保存(从关联中删除已删除的对象):[com.......SomeObject#353296]。我认为这可能由两个因素引起:在您的(Xstream创建的)对象图中,如果使用JPA进行级联检索,则缺少一个子实体实例:这会导致对象删除。在您的(Xstream创建的)对象图的其他地方,存在相同的子实体:这会导致对象被重新保存。如何调试:(a)从Xstream创建对象图并完全打印出来-每个实体,每个字段;(b)通过JPA加载相同的对象图,从顶部实体级联检索并完全打印出它们-每个实体,每个字段(c)比较两者-是否从(a)中缺少了(b)中存在的某些内容?

如果不必获取会话中的持久对象并修改它,而是修改瞬时对象并尝试保存并覆盖现有的持久对象,那么是否有实现我想要做的事情的方法?

通过刚才建议的调试/修复 - 希望能够解决。

或者我的强制性建议(愚笨地)绕过此错误(但会稍微降低性能):在填充对象图中的ID后,调用EntityManager.clear(),然后继续使用merge()和saveOrUpdate()。但是,对数据库结果进行单元测试,以确保您在填充Xstream图形时没有忽略重要内容。

作为测试/最后的手段,请尝试不使用merge()的saveOrUpdate(),但是您可能需要清除()实体管理器以避免Hibernate异常,例如NonUniqueObjectException。

如果您获得更多信息/更多信息,请告诉我B^)


我已经尝试过使用clear()和saveOrUpdate()而不是merge(),但它们会产生其他Hibernate错误...我认为这些错误可能包括实体副本已存在和瞬态对象实例化。我将尝试您提供的其他建议。非常感谢您详细的回答。 - eipark
如何“打印”整个对象?这实际上是我正在解决的原始问题,而XStream就是在这个问题中发挥作用的地方。 - eipark

1
    Hibernate merge was not meant for associating transient objects 
to persistent ones.

由于您实际上正在处理分离的对象,因此合并(merge())应该对您有用。由于在调用合并之前在“transObj”中设置了ids,因此Hibernate会将它们视为分离的(而不是瞬态的)。

我认为您代码的问题在于它使Hibernate出现了问题。

在您的代码中,您正在从数据库加载“persistObj”。现在,Hibernate将此“persistObj”保存在会话中。然后,您将来自“persistObj”的一些ids设置在“transObj”中,然后调用合并。在“persistObj”和“transObj”中,一些子对象具有相同的id,这让Hibernate感到困惑。

ComplexObject transObj = xstream.import("object.xml");
ComplexObject persistObj = someService.getObjByName(transObj.getName());
for (OtherObject o : c.getObjects()) {
    if (persistObj.getObjects().contains(o.getName())) {
        o.setId(persistObj.getObjectByName(o.getName()).getId())
     }
    ... set a bunch of other IDs deeper in the object graph ...
}
transObj = session.merge(transObj);
session.saveOrUpdate(transObj);

尝试在加载'persistObj'后调用session.clear(),这样Hibernate将删除persistObj并仅考虑您的分离对象。
ComplexObject transObj = xstream.import("object.xml");
ComplexObject persistObj = someService.getObjByName(transObj.getName());
// Clear the session, so that hibernate removes 'persistObj' from it's cache
session.clear();
for (OtherObject o : c.getObjects()) {
    if (persistObj.getObjects().contains(o.getName())) {
        o.setId(persistObj.getObjectByName(o.getName()).getId())
     }
    ... set a bunch of other IDs deeper in the object graph ...
}
transObj = session.merge(transObj);
session.saveOrUpdate(transObj);

1
抱歉,没有看到另一个留言。是的,请发布使用clear()时看到的错误。我绝对不会将'persistObt'和'tranObj'附加到同一个会话上,因为您正在尝试使用两个不同的对象实例表示同一个数据库行。Hibernate通常不允许这样做。 - Sashi
在调用session.merge()之前,我立即调用了session.clear()。 - eipark
在我的merge()中,我得到了java.lang.IllegalStateException: An entity copy was already assigned to a different entity。尽管在合并之前调用了clearSession(),但仍然出现了这个问题。 - eipark
1
看起来 Hibernate 在调用 session.clear() 后仍然对旧对象有某种引用。我建议您重构代码,使其在一个 Hibernate 会话中加载 'persistObj'。在事务范围之外使用 'persistObj' 来填充您的 ID。然后在新的 Hibernate 会话中调用 'saveOrUpdate'(不是 merge)。 - Sashi
是的,看起来是这样。我会试一试。 - eipark
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接