Unidirectional @OnetoMany mapping deletes all relationships and re-adds remaining ones rather than removing the specific one

huangapple go评论83阅读模式
英文:

Unidirectional @OnetoMany mapping deletes all relationships and re-adds remaining ones rather than removing the specific one

问题

以下是翻译好的内容:

给定以下代码:

public class Course {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Review> reviews = new ArrayList<>();
}

public class Review {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String rating;
    private String description;
}

保存了带有两个评论的课程。

如果我尝试从课程中移除一个评论。

course.getReviews().remove(0);

Hibernate 发出以下查询。

delete from course_reviews where course_id=? 
binding parameter [1] as [BIGINT] - [1]
insert into course_reviews (course_id, reviews_id) values (?, ?) 
binding parameter [1] as [BIGINT] - [1]
binding parameter [2] as [BIGINT] - [3]

请注意,它首先删除了所有的关联,然后再插入剩下的。为什么会出现这种行为?为什么它不能更具体地只删除存储关系的那条记录。

英文:

Given the following code

public class Course {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List&lt;Review&gt; reviews = new ArrayList&lt;&gt;();
}

public class Review {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String rating;
    private String description;
}

Saved course with 2 reviews.

If I try to remove one review from course.

course.getReviews().remove(0);

Hibernate fires following queries.

delete from course_reviews where course_id=? 
binding parameter [1] as [BIGINT] - [1]
insert into course_reviews (course_id, reviews_id) values (?, ?) 
binding parameter [1] as [BIGINT] - [1]
binding parameter [2] as [BIGINT] - [3]

Notice that it deletes all the relationships first and then inserts the remaining. Why this behavior? Why couldn't it be more specific and delete just that one record storing the relationship.

答案1

得分: 3

不确定这是否是由于包的语义(因为您在评论中使用的是List而不是Set)还是因为 Hibernate 有时会进行所谓的“集合重建”。尝试使用Set

英文:

Not sure if this is due to bag semantics(because you use a List rather than Set for reviews) or just because Hibernate sometimes does so called "collection recreations". Try using a Set.

答案2

得分: 2

Hibernate之所以这样做是因为它不知道实体之间的关系。由于没有关系标识的信息,它使用唯一拥有的信息 - 内存中的对象。因此,它通过谓词清除表格,并将内存中的实体持久化。

您需要在子实体一侧使用@JoinColumn,并在父实体一侧使用@OneToManymappedBy参数。

英文:

Hibernate does that because it has no idea about how the entities are related. Since there is no information about how relations are identified, it uses the only information it has - objects in the memory. So it clears the table by the predicate and persists the entities from memory.

You need to use @JoinColumn on the child side and mappedBy parameter of @OneToMany on the parent side.

答案3

得分: 1

首先,您在文档中可以找到描述您所看到行为的信息:

> 单向关联在删除子实体方面效率不高。在上面的示例中,刷新持久化上下文时,Hibernate 会删除与父实体 Person 相关联的链接表(例如 Person_Phone)中的所有数据库行,并重新插入仍然在 @OneToMany 集合中找到的行。
>
> 另一方面,双向的 @OneToMany 关联要高效得多,因为子实体控制着关联。

至于问题:
> 为什么会出现这种行为?为什么不能更具体,只删除存储关系的那条记录。

答案并不简单,需要深入研究 Hibernate 源代码。

Hibernate 中实体集合处理的关键点在于 PersistentCollection 接口。正如该接口的注释中所述:

> Hibernate 会将 Java 集合包装在 PersistentCollection 的实例中。这个机制旨在支持跟踪集合持久状态的更改和集合元素的延迟实例化。不利之处在于仅支持某些抽象集合类型,并且丢失了任何额外的语义。

在我们的讨论中,这个接口的以下方法非常重要:

/**
  * 当更改发生时,我们是否需要完全重新创建此集合?
  *
  * @param persister 集合持久化器
  * @return 如果更改需要重新创建,则为 {@code true}。
  */
boolean needsRecreate(CollectionPersister persister);

Hibernate 会在刷新时创建一个操作队列,用于安排创建/删除/更新操作(参见 AbstractFlushingEventListener.flushCollections 方法)。因此,我们的集合属于此队列中的 CollectionUpdateAction 操作之一。

CollectionUpdateAction.execute() 方法的实现中可以看出,Hibernate 根据 collection.needsRecreate(persister) 调用来检查是否需要重新创建集合。

PersistentCollection 接口以下的实现层次结构:

PersistentCollection
   |
   |-- AbstractPersistentCollection
           |
           |-- PersistentArrayHolder
           |-- PersistentBag
           |-- PersistentIdentifierBag
           |-- PersistentList
           |-- PersistentMap
                  |
                  |-- PersistentSortedMap
           |
           |-- PersistentSet
                  |
                  |-- PersistentSortedSet

实际上,needsRecreate 方法仅在 AbstractPersistentCollection 中实现,并针对 PersistentBag 进行了如下覆盖:

@Override
public boolean needsRecreate(CollectionPersister persister) {
	return !persister.isOneToMany();
}

Hibernate 在解析您的领域模型时会决定集合属于上述层次结构中的哪种类型。

  1. 当您使用您问题中描述的映射时:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List&lt;Review&gt; reviews;

Hibernate 将将其视为 PersistentBag,并且 PersistentCollection.needsRecreate 方法返回 true(因为使用了 BasicCollectionPersister)。

  1. 您可以使用 @OrderColumn 注解
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@OrderColumn
private List&lt;Review&gt; reviews;

在这种情况下,集合将被视为 PersistentList,您将避免重新创建集合。但是,这还需要在 Course_Review 表中添加额外的 order column(必须是整数类型)。当您尝试从列表开头删除项目时,还会进行大量的顺序列更新。

  1. 您可以使用 Set 接口替换 List(正如 Christian Beikov 所指出的):
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set&lt;Review&gt; reviews;

在这种情况下,集合将被视为 PersistentSet,您也将避免重新创建集合。当使用 Set 时,为子实体提供适当的 equals/hashCode 实现非常重要。更好的 equals/hashCode 实现会利用自然 ID 或业务键。您只能通过对象引用从该集合中删除项目,因为 remove(int index) 方法在 Set 接口中并不存在。

英文:

First of all the behavior that you see is described in the documentation:

> The unidirectional associations are not very efficient when it comes to removing child entities. In the example above, upon flushing the persistence context, Hibernate deletes all database rows from the link table (e.g. Person_Phone) that are associated with the parent Person entity and reinserts the ones that are still found in the @OneToMany collection.
>
> On the other hand, a bidirectional @OneToMany association is much more efficient because the child entity controls the association.

As for the question:
> Why this behavior? Why couldn't it be more specific and delete just that one record storing the relationship.

The answer is not so simple and require deep diving into the hibernate source code.

The key point of the entity's collection processing in hibernate is the PersistentCollection interface. As it stated in the comments to this interface:

> Hibernate wraps a java collection in an instance of PersistentCollection. This mechanism is designed to support tracking of changes to the collection's persistent state and lazy instantiation of collection elements. The downside is that only certain abstract collection types are supported and any extra semantics are lost.

The important place in our discussion have the following method of this interface:

/**
  * Do we need to completely recreate this collection when it changes?
  *
  * @param persister The collection persister
  * @return {@code true} if a change requires a recreate.
  */
boolean needsRecreate(CollectionPersister persister);

Hibernate creates an action queue for scheduling creates/removes/updates at flushing time (see the AbstractFlushingEventListener.flushCollections method). So, our collection belongs to one of the CollectionUpdateAction action in this queue.

As you can see from the CollectionUpdateAction.execute() method implementation, hibernate checks need of a collection recreation based on the on the collection.needsRecreate(persister) call.

The PersistentCollection interface has the following hierarchy of implementations:

PersistentCollection
   |
   |-- AbstractPersistentCollection
           |
           |-- PersistentArrayHolder
           |-- PersistentBag
           |-- PersistentIdentifierBag
           |-- PersistentList
           |-- PersistentMap
                  |
                  |-- PersistentSortedMap
           |
           |-- PersistentSet
                  |
                  |-- PersistentSortedSet

Actually, the needsRecreate method implemented only in the AbstractPersistentCollection and overridden for the PersistentBag in the following way:

@Override
public boolean needsRecreate(CollectionPersister persister) {
	return !persister.isOneToMany();
}

Hibernate decides to what type from the above hierarchy a collection belongs at time of parsing your domain model.

  1. When you use the described in your question mapping:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List&lt;Review&gt; reviews;

hibernate will treat it as PersistentBag and the method PersistentCollection.needsRecreate returns true (because the BasicCollectionPersister is used).

  1. You can use the @OrderColumn annotation:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@OrderColumn
private List&lt;Review&gt; reviews;

in this case the collection will be treated as the PersistentList and you will avoid the collection recreation. But this is also required additional order column (must be of integral type) in the Course_Review table. And when you will try to remove an item from the beginning of the list you will have also a lot of the order columns updates.

  1. You can use the Set interface instead of List (as was noticed by Christian Beikov):
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set&lt;Review&gt; reviews;

in this case the collection will be treated as the PersistentSet and you will avoid the collection recreation as well. When using Sets, it’s very important to supply proper equals/hashCode implementations for child entities. A better equals/hashCode implementation, making use of a natural-id or business-key. And you will be able to remove an item from this collection only by the object reference as the method remove(int index) just absent in the Set interface.

huangapple
  • 本文由 发表于 2020年10月15日 08:44:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/64363313.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定