单向@OnetoMany 映射删除所有关系并重新添加剩余关系而不是删除特定关系

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;
}

保存的课程有 2 条评论。

如果我尝试从课程中删除一条评论。

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]

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

Hibernate 这样做是因为它不知道实体是如何相关的。由于没有关于如何识别关系的信息,它使用它拥有的唯一信息——内存中的对象。因此它通过谓词清除 table 并从内存中保留实体。

您需要在子端使用 @JoinColumn,在父端使用 @OneToManymappedBy 参数。

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

首先,您在 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.

关于问题:

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

答案并不那么简单,需要深入研究休眠源代码。

实体在hibernate中的集合处理的关键点是PersistentCollection接口。正如该界面的评论中所述:

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.

我们讨论的重要地方有这个接口的如下方法:

/**
  * 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/removes/updates(请参阅此队列中的 AbstractFlushingEventListener.flushCollections method). So, our collection belongs to one of the 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<Review> reviews;

hibernate 会将其视为 PersistentBag 并且方法 PersistentCollection.needsRecreate returns true(因为使用了 BasicCollectionPersister)。

  1. 您可以使用 @OrderColumn annotation:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@OrderColumn
private List<Review> reviews;

在这种情况下,集合将被视为 PersistentList,您将避免重新创建集合。但是这也需要在Course_Reviewtable中附加顺序列(必须是整数类型)。当您尝试从列表的开头删除一个项目时,您也会有很多订单列更新。

  1. 您可以使用 Set 界面而不是 List(正如 Christian Beikov 所注意到的):
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Review> reviews;

在这种情况下,集合将被视为 PersistentSet,您也将避免重新创建集合。使用 Sets 时,它是 very important to supply proper equals/hashCode implementations for child entities. A better equals/hashCode 实现,使用 natural-id 或 business-key。并且您将只能通过对象引用从该集合中删除一个项目,因为方法 remove(int index)Set 接口中不存在。