JPA 级联持续存在并且对分离实体的引用会引发 PersistentObjectException。为啥?

Posted

技术标签:

【中文标题】JPA 级联持续存在并且对分离实体的引用会引发 PersistentObjectException。为啥?【英文标题】:JPA cascade persist and references to detached entities throws PersistentObjectException. Why?JPA 级联持续存在并且对分离实体的引用会引发 PersistentObjectException。为什么? 【发布时间】:2011-05-16 17:33:50 【问题描述】:

我有一个引用实体 Bar 的实体 Foo:

@Entity
public class Foo 

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

当我持久化一个新的 Foo 时,它可以获得对新 Bar 或现有 Bar 的引用。当它获得一个碰巧分离的现有 Bar 时,我的 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 的引用受到管理(附加)或当我在关系中省略级联 PERSIST 时,一切正常。

然而,这两种解决方案都不是 100% 令人满意的。如果我删除级联持久化,我显然不能再持久化一个引用新 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 不会改变,而且这个 ID 似乎是这里唯一需要的东西。

这可能是 Hibernate 中的错误还是我遗漏了什么?

【问题讨论】:

您可以从级联中删除 PERSIST,并检查 if (id == null) em.persiste(foo.getBar()) 至少它使您的 if 更简单。 没错,这确实会使它稍微简单一些。感谢您的建议。 当然,即使使用稍微简单的 if 语句,持久化代码仍然需要遍历整个对象图,这是我极力避免的事情。 【参考方案1】:

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

foo = entityManager.merge(foo); 

当应用于新实例时,merge() 使其持久化(实际上 - 返回具有相同状态的持久化实例),并合并级联引用,就像您尝试手动执行的操作一样。

【讨论】:

确实,这似乎可以解决问题。 merge() 基本上可以作为传统的持久化和更新操作。我确实认为 javadoc 在这个问题上可能会更清楚一点。我仍然不完全理解为什么 persist() 坚持要管理引用,即使似乎没有理由这样做,但实际上 merge() 似乎是一个不错的选择。 是否可以通过其他条件而不是通过 id 来进行“合并”?我从外部来源获取对象,如果名称和街道生日匹配,我想检索该对象。但是对象在级联持久树中更深,所以我不会手动从数据库加载。 那么有一个“合并”方法和一个“持久”方法有什么用呢?在这种情况下,我们必须使用“合并”方法来创建一个对象,这很奇怪。还是我错过了什么?【参考方案2】:

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

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

编辑:

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

【讨论】:

你是对的。 Bar 引用,IFF 它是一个现有实体,只需要允许新的 Foo 具有对 Bar 的 FK。在这种情况下, getReference() 看起来确实是 find 或 merge() 的一种更便宜的替代方法。然而问题仍然存在,我不喜欢手动遍历对象图并将所有分离的引用替换为附加的引用。我仍然不明白为什么 JPA 坚持使用托管引用,或者为什么聚合根上没有递归“全部附加”。 应该让 JPA 提供者直接访问该字段而不是通过 getter 和 setter 更有效。 以前没有听说过,但感谢提示! 它并不是对所有 JPA 实现都更有效,也许只是您正在使用的那个。其他(例如 DataNucleus)通过两种方式提供相同的效率

以上是关于JPA 级联持续存在并且对分离实体的引用会引发 PersistentObjectException。为啥?的主要内容,如果未能解决你的问题,请参考以下文章

EntityManager 是不是持有对分离实体的引用?

JPA 级联来自多个persistenceUnits 的实体

删除分离实体spring jpa Repository接口

Spring data JPA:如何启用级联删除而不引用父级中的子级?

JPA 错误未标记级联 PERSIST 的关系

JPA 级联持续错误