Hibernate - 拥有cascade=”all-delete-orphan”集合的实例不再被所属的实体引用。

364

我在尝试更新实体时遇到以下问题:

"A collection with cascade=”all-delete-orphan” was no longer referenced by the owning entity instance".

我有一个父实体,它拥有一组某些子实体的Set<...>。当我尝试更新它时,我将所有引用设置为该集合并进行设置。

下面的代码表示我的映射:

@OneToMany(mappedBy = "parentEntity", fetch = FetchType.EAGER)
@Cascade({ CascadeType.ALL, CascadeType.DELETE_ORPHAN })
public Set<ChildEntity> getChildren() {
    return this.children;
}

我尝试根据这个链接:如何“可能”解决问题,仅清理Set<..>,但它没有起作用。

如果您有任何想法,请告诉我。

谢谢!


4
@mel3kings,您提供的链接已经失效。 - Opal
尝试在删除元素时使用可变集合。例如,如果manyother是一个List<T>,不要使用something.manyother.remove(other)。将manyother变为可变的,如ArrayList<T>,并使用orphanDelete = true - nurettin
有一个看起来非常相似的 bug:https://hibernate.atlassian.net/browse/HHH-9940,以及重现它的代码:https://github.com/abenneke/sandbox/tree/master/hibernate-null-collection/src/test。 - ACV
3
链接“How to 'possible'解决问题”不再可用。 - Adrian
2
如何“可能”解决Internet Archive中的“如何‘可能’解决问题”链接...(https://web.archive.org/web/20171010150609/http://www.onkarjoshi.com/blog/188/hibernateexception-a-collection-with-cascade-all-delete-orphan-was-no-longer-referenced-by-the-owning-entity-instance/comment-page-1/#comment-9994) - Christian
33个回答

294

检查所有给sonEntities赋值的地方。你所引用的链接明确指出要创建一个新的HashSet,但每当重新分配set时都可能会出现此错误。例如:

public void setChildren(Set<SonEntity> aSet)
{
    this.sonEntities = aSet; //This will override the set that Hibernate is tracking.
}

通常情况下,您只需要在构造函数中创建一次"set"。每当您想要添加或删除列表中的某些内容时,您需要修改列表的内容而不是分配一个新的列表。

添加子项:

public void addChild(SonEntity aSon)
{
    this.sonEntities.add(aSon);
}

移除子元素:

public void removeChild(SonEntity aSon)
{
    this.sonEntities.remove(aSon);
}

10
实际上,我的问题与实体的equals和hashcode有关。遗留代码可能会带来很多问题,永远不要忘记检查它。我所做的只是保持删除孤儿策略并修正equals和hashcode。 - axcdnt
8
我很高兴你解决了问题。在使用Hibernate时,equals和hashcode曾经让我遇到过一些问题。你不应该只是在问题标题中加上“[已解决]”,而是应该发布你的答案并将其标记为被接受的答案。 - brainimus
谢谢,我遇到了类似的问题,结果发现我的那个Set的setter是空的。 - Siddhartha
6
通常在构造函数中只需要一次性使用 "new" 来创建集合。如果想要添加或删除集合中的元素,应该修改集合的内容而不是重新赋值一个新的集合。 - Nikhil Sahu
谢谢你,你帮我节省了很多时间!我在我的setter中设置新的集合,但那样做破坏了一切。我只是按照您的建议改变了逻辑,根据传递的参数筛选了列表。 - Khachatur Stepanyan
显示剩余3条评论

147

该方法:

public void setChildren(Set<SonEntity> aSet) {
    this.sonEntities = aSet;
}

如果parentEntity未与特定上下文分离,即查找和更新操作在同一事务中,则以下方法有效。但是,如果实体未与上下文分离,则无法正常工作,并且需要先将其分离再进行更新。

public void setChildren(Set<SonEntity> aSet) {
    //this.sonEntities = aSet; //This will override the set that Hibernate is tracking.
    this.sonEntities.clear();
    if (aSet != null) {
        this.sonEntities.addAll(aSet);
    }
}

2
@Skuld 我有一个类似的问题,我尝试了你的解决方案(在setter方法中清除children集合 - this.children.clear() - 然后添加新的children - this.children.addAll(children))。但是这个改变没有解决我的问题。我仍然得到"A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance"异常。你有任何想法吗?非常感谢! - ovdsrn
1
这个方法很好用,但是当子元素包含在嵌套的Hibernate组件中并且该组件在其实体中被设置为null时,它就不太好用了。然后你会得到相同的错误。这是因为子元素的所有者是根实体而不是被设置为null的组件... 因此,组件永远不允许变成null,而应该导致子元素调用destroy()方法... 至少我不知道更好的解决方案... 这是一种类似于通过destroy()清除组件集合的构造方式... - edbras
请参阅此帖子以获取有关上述内容的更多详细信息:https://dev59.com/dFPTa4cB1Zd3GeqPk6Kw - edbras
9
使用this.sonEntities.retainAll(aSet)而不是sonEntities.clear(),因为如果aSet == this.sonEntities(即相同的对象),你将在向其添加元素之前清空集合! - Martin
这就是我所需的 "this.sonEntities.clear();" 感谢 @Manu。 - Mehdi Bouzidi
显示剩余3条评论

44

当我在各个地方读到 Hibernate 不喜欢你给集合赋值时,我假设最安全的做法显然是像这样将其设置为 final:

当我阅读各处关于Hibernate不喜欢你给一个集合分配值的信息时,我认为最安全的做法显然是像这样将其声明为final:

class User {
  private final Set<Role> roles = new HashSet<>();

public void setRoles(Set<Role> roles) {
  this.roles.retainAll(roles);
  this.roles.addAll(roles);
}
}

然而,这种方法行不通,你会得到令人恐惧的“不再被引用”错误提示,实际上在这种情况下这个错误提示非常具有误导性。

事实证明,Hibernate 调用了你的 setRoles 方法,并且它希望在此安装它的特殊集合类,而不接受你的集合类。尽管看过所有有关不在 set 方法中分配集合的警告,但我仍困惑了很长时间。

所以我改成了这个:

public class User {
  private Set<Role> roles = null;

  public void setRoles(Set<Role> roles) {
  if (this.roles == null) {
    this.roles = roles;
  } else {
    this.roles.retainAll(roles);
   this.roles.addAll(roles);
  }
}
}
因此,在第一次调用时,Hibernate会安装其特殊的类,并在后续的调用中,您可以使用该方法而不会破坏任何东西。如果您想将自己的类用作bean,则可能需要一个有效的setter,而至少这似乎是有效的。

2
你为什么要调用retainAll()?为什么不使用clear()后再addAll()呢? - edbras
1
为什么要调用retainAll()?为什么不使用clear()后再addAll()呢?这是个好问题,我认为我之前的想法是如果你在添加新项之前不删除旧项,hibernate可能会将其视为数据库更新的可能性较小。但我怀疑它并不会这样工作。 - xpusostomos
2
你应该使用retainAll()而不是clear(),否则如果你恰好传入相同的set对象,你可能会清除角色。例如:user.setRoles(user.getRoles()) == user.roles.clear() - Martin
3
当你设置orphanRemoval=true时,如果创建一个记录并且该集合为null,也会出现这个错误。 因此:a具有oneToMany b,并设置orphanRemoval=true。当你创建B=null的A时,就会触发这个问题。你的解决方案是初始化并使其为final似乎是最好的。 - Lawrence
谢谢!我将我的List初始化为 List<String> list = new ArrayList<>();。把它改成 List<String> list = null;就解决了问题 :) - Radical
实际上,如果您只删除final修饰符,您答案中的第一个代码块就可以工作。这样做的好处是:(1)当您在代码中使用new创建实体时,roles将是HashSet实例,这对于Hibernate持久化新实体完全没有问题。(2)当您更新托管实体时,roles将成为受管理的Hibernate代理实例,因为它将被分配到实体加载的字段上(因为它不是final)。(3)当您合并临时实体时,也没有问题。 - Ruslan Stelmachenko

34

我通过以下方式进行了修复:

1. 清空现有的子元素列表,使它们从数据库中删除

parent.getChildren().clear();

2. 将上面创建的新子列表添加到现有列表中

parent.getChildren().addAll(children);

希望这篇文章能帮助你解决错误。


1
对我有用...谢谢。 - KPK
@KPK 不用谢 - Ousama
除了上面的代码之外,我还不得不将存储库从CrudRepository修改为JpaRepository,并更改用户saveAndFlush()调用。一个是在删除所有内容后,另一个是在添加新内容后。 - Nikhil

29

实际上,我的问题与实体的equals和hashcode有关。遗留代码可能会带来很多问题,永远不要忘记检查它。我所做的只是保持删除孤儿策略并修正equals和hashcode。


16
请给出一个写得好的equals和hashCode方法的例子吗?因为我遇到了很多问题:要么我不能更新我的set,要么我会得到StackOverflow错误。我在这里开了个问题:http://stackoverflow.com/questions/24737145/equals-and-hashcode-of-these-entities-spring-mvc-hibernate 。谢谢。 - SaganTheBest

17

我也遇到了同样的错误。对我来说问题是,在保存实体后,映射的集合仍然为空,当尝试更新实体时会抛出异常。对我有帮助的是:先保存实体,然后进行刷新(集合不再为空),然后执行更新。也许使用new ArrayList()之类的方法初始化集合也可能有所帮助。


2
发送新的ArrayList而不是null,对我有用。谢谢 - rpajaziti

8

我在使用JSON POST请求更新实体时遇到了这个问题。 当我没有关于子元素的数据更新实体时,即使没有子元素也会出现错误。 加上

"children": [],

通过修改请求主体来解决问题。


6

1
仍然发生在使用5.5.7.Final时。 当要合并的实体从Web会话中反序列化时,例如从JSF前端合并更改到应该进行合并的后端服务时,就会出现问题。 将enableLazyInitialization设置为false可以解决此错误,但缺点是无法使用延迟初始化功能,这有点烦人。 - Matthias

5
我采用了@user2709454的方法并进行了小改进。
public class User {
    private Set<Role> roles;

    public void setRoles(Set<Role> roles) {
        if (this.roles == null) {
            this.roles = roles;
        } else if(this.roles != roles) { // not the same instance, in other case we can get ConcurrentModificationException from hibernate AbstractPersistentCollection
            this.roles.clear();
            if(roles != null){
                this.roles.addAll(roles);
            }
        }
    }
}

5

所有这些答案都没能帮助我,但是我找到了另一个解决方案。

我有一个包含实体B列表的实体A。实体B包含一个实体C列表。

我试图更新实体A和B,它起作用了。但是在更新实体C时,我遇到了上述错误。在实体B中,我有一个注释,像这样:

@OneToMany(mappedBy = "entity_b", cascade = [CascadeType.ALL] , orphanRemoval = true)
var c: List<EntityC>?,

我只是删除了 orphanRemoval,更新就成功了。


1
嗨@IonicMan,在删除孤立的移除之后,我现在不再遇到上述错误了,但是我遇到了下面提到的新错误。 对象引用一个未保存的瞬态实例——请在刷新之前保存该瞬态实例。 - Tejal
@Tejal 很棒,很高兴我能帮到你。 - IonicMan
是的,但现在你的行为完全不同了。如果你移除注释,孤立的实体将不再被移除。这意味着你的数据库中将会有一些EntityC的条目,它们现在引用为空(指向EntityB)。当C真正依赖于B时,这很少是你想要的情况... - Mario B
@MarioB 我们终究都会死去,所以谁在乎呢 - IonicMan
有趣的态度。我猜如果你不在乎空条目,那就没关系了。 不过,让人们意识到这种解决方案的缺点是很好的——对于许多使用情况来说,这似乎是危险的建议。 - Mario B
@MarioB 挑战使我们变得更强大,我们每个人都会个别决定如何最好地应对生活。在我看来,最好的方式是以轻松的态度来应对,并意识到我们是舞台上的演员。演出将会结束,新的演员将登台。因此,放松自己是有帮助的。这也将导致更具创造力的更好解决方案。这是我对本评论读者的建议。 - undefined

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