【问题标题】:Unidirectional @OnetoMany mapping deletes all relationships and re-adds remaining ones rather than removing the specific one单向 @OnetoMany 映射删除所有关系并重新添加剩余的关系,而不是删除特定的关系
【发布时间】:2021-01-29 10:48:24
【问题描述】:

给定以下代码

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]

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

【问题讨论】:

    标签: java hibernate jpa one-to-many hibernate-mapping


    【解决方案1】:

    首先,您看到的所有行为都在documentation 中进行了描述:

    单向关联在删除子实体时效率不高。在上面的示例中,在刷新持久性上下文时,Hibernate 会从链接表(例如 Person_Phone)中删除与父 Person 实体关联的所有数据库行,并重新插入仍然在 @OneToMany 集合中找到的行。

    另一方面,双向@OneToMany 关联效率更高,因为子实体控制关联。

    至于问题:

    为什么会有这种行为?为什么不能更具体一点,只删除存储关系的那条记录。

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

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

    Hibernate 包装PersistentCollection 实例中的java 集合。此机制旨在支持跟踪对集合的持久状态的更改和集合元素的延迟实例化。缺点是仅支持某些抽象集合类型,并且丢失了任何额外的语义。

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

    /**
      * 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 创建一个操作队列,用于在刷新时调度创建/删除/更新(请参阅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<Review> reviews;
    

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

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

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

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

    在这种情况下,集合将被视为PersistentSet,您也将避免重新创建集合。使用 Set 时,very important 为子实体提供正确的 equals/hashCode 实现。 better equals/hashCode 实现,使用自然 ID 或业务密钥。并且您将能够仅通过对象引用作为 remove(int index) 方法在 Set 接口中不存在的方法从该集合中删除一个项目。

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 谢谢!这行得通。但是为什么即使动作完全相同,它也会对 Set 和 List 表现出如此不同的行为。你能指点我一些文章,我可以在其中阅读有关此集合娱乐的文章
      • 问题是当你有包语义时,即“多个可能的具有相同值的行”,你不能只执行 delete 语句来删除“第一个”元素。所以 Hibernate 必须删除所有元素,然后重新插入它们。
      【解决方案3】:

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

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

      【讨论】:

      • 但对象确实包含信息。课程和已删除的评论都在 itd 中填充了 ID。在删除查询“delete from course_reviews where course_id=?”中需要更具体的reviews_id在删除时已经知道休眠,无法理解为什么休眠无法解决这个问题。
      • @KP- 没有任何信息。您需要了解休眠映射的工作原理。看看这篇文章:vladmihalcea.com/…
      • 感谢您的回复。但是,如果我只是将其更改为 Set 而不是 List,它会触发更具体的查询“从 course_reviews 中删除 course_id=? 和 reviews_id=?”
      • @KP- 没问题。当然,这是一个简单的解决方法。不过,您很快就会遇到性能问题;)
      • 这种映射方式绝对不是映射一对多的正确方式,我同意你的 cmets。但我的问题更具体是关于删除和重新添加关系:)
      猜你喜欢
      • 2016-09-03
      • 2013-11-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多