JPA级联持久化和对已分离实体的引用会抛出PersistentObjectException异常。为什么?

33

我有一个实体Foo,它引用了一个实体Bar:

@Entity
public class Foo {

    @OneToOne(cascade = {PERSIST, MERGE, REFRESH}, fetch = EAGER)
    public Bar getBar() {
        return bar;
    }
}

当我持久化一个新的Foo时,它可以引用一个新的Bar或一个现有的Bar。当它引用一个现有的Bar并且该Bar已经被分离(detached),我的JPA提供者(Hibernate)会抛出以下异常:

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.Bar
 at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102)
 at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:636)
 at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:628)
 at org.hibernate.engine.EJB3CascadingAction$1.cascade(EJB3CascadingAction.java:28)
 at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:291)
 at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:239)
 at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
 at org.hibernate.engine.Cascade.cascade(Cascade.java:153)
 at org.hibernate.event.def.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:454)
 at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:288)
 at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
 at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:130)
 at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:49)
 at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:154)
 at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:110)
 at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:61)
 at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:645)
 at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:619)
 at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:623)
 at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:220)
 ... 112 more

当我确保对Bar的引用是被管理的(附加的)或者在关系中省略CASCADE PERSIST时,一切都很顺利。

但是这两种解决方案都不是百分之百的满意。如果我删除级联持久性,我显然不能再将一个新的Bar的引用与Foo一起持久化。使Bar的引用得到管理需要在持久化之前编写如下代码:

if (foo.getBar().getID() != null && !entityManager.contains(foo.getBar())) {
    foo.setBar(entityManager.merge(foo.getUBar()));
}
entityManager.persist(foo);

对于单个Bar来说,这似乎并不是什么大问题,但是如果我需要考虑所有属性,我最终将得到相当可怕的代码,这似乎违背了首先使用ORM的原因。 我可能要像以前一样使用JDBC手动持久化对象图。

当给定现有的Bar引用时,JPA所要做的唯一事情就是取其ID并将其插入保存Foo的表的列中。 当Bar被附加时,它确实执行此操作,但在Bar分离时抛出异常。

我的问题是;为什么它需要Bar被附加? 当Bar实例从分离状态转换为附加状态时,它的ID肯定不会改变,而且这似乎是唯一需要的东西。

这可能是Hibernate中的一个错误吗?还是我漏看了什么?


2
你可以从级联中移除PERSIST,并检查if (id == null) { em.persiste(foo.getBar()) }至少这会使得你的if语句更简单。 - amorfis
真的,这确实会让它稍微简单一些。感谢您的建议。 - Arjan Tijms
当然,即使使用稍微简单的if语句,持久化代码仍然必须遍历整个对象图,这是我拼命想要避免的。 - Arjan Tijms
2个回答

21

在这种情况下,您可以使用 merge() 替换 persist()

foo = entityManager.merge(foo); 

使用merge()方法对新实例进行操作会使其持久化(实际上是返回具有相同状态的持久化实例),并合并级联引用,就像您手动尝试做的那样。


确实,那似乎就是解决办法。merge()似乎基本上可以作为传统的持久化和更新操作。我认为javadoc在这个问题上可能应该更清楚一些。虽然我仍然不完全理解为什么persist()要求引用被管理,即使似乎没有理由,但实际上merge()似乎是一个相当好的替代方法。 - Arjan Tijms
可以按照除id之外的其他条件进行“合并”吗?我从外部源获取对象,如果名称、街道和生日匹配,我想检索该对象。但是该对象深入到级联持久化树中,因此我不会手动从数据库中加载。 - flaschenpost
那么拥有“合并”方法和“持久化”方法有什么用呢?在这种情况下,我们必须使用“合并”方法来创建一个对象,这相当奇怪。或者我错过了什么吗? - Mat

7
如果我理解正确,您只需要Bar引用来允许新的Foo在持久化时具有外键值(指向现有的Bar)。在这种情况下,EntityManager上有一个名为getReference()的JPA方法可能对您有用。 getReference()方法类似于find(),不同之处在于它不会返回托管实例(Bar)除非它已经缓存在持久性上下文中。它将返回一个代理对象,以满足您的外键需求,以便持久化Foo对象。我不确定这是否是您希望得到的解决方案,但请尝试一下,看看是否适用于您。

我还从您的代码中注意到,您正在使用“属性”样式访问而不是通过注释getter方法(用于Bar关系)。有什么原因吗?出于性能原因,建议您注释成员而不是getter。JPA提供程序直接访问字段比通过getter和setter更有效率。

编辑:

正如其他人提到的那样,使用级联merge()将持久化新实体以及合并修改的实体并重新附加具有MERGE级联选项关系的分离实体。使用PERSIST级联选项不会重新附加或合并任何内容,并且应在需要该行为时使用。


你说得对。如果Bar引用是一个现有实体,则仅需要允许新的Foo具有FK到Bar。在这种情况下,getReference()看起来确实是find或merge()的更便宜的替代方法。然而问题仍然存在,我不喜欢手动遍历对象图并用附加的对象替换所有分离的引用。我还不明白为什么JPA坚持拥有受控引用,或为什么聚合根上没有递归的“全部附加”的可能性。 - Arjan Tijms
据说JPA提供程序直接访问字段比通过getter和setter更有效率。以前没有听说过,但感谢您的提示! - Arjan Tijms
2
并不是所有的JPA实现都更加高效,可能只有你正在使用的那个。其他的实现(例如DataNucleus)则可以同时提供这两种方式的效率。 - DataNucleus

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