英文:
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<Review> reviews = new ArrayList<>();
}
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
,并在父实体一侧使用@OneToMany
的mappedBy
参数。
英文:
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 在解析您的领域模型时会决定集合属于上述层次结构中的哪种类型。
- 当您使用您问题中描述的映射时:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews;
Hibernate 将将其视为 PersistentBag
,并且 PersistentCollection.needsRecreate
方法返回 true
(因为使用了 BasicCollectionPersister)。
- 您可以使用
@OrderColumn
注解:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@OrderColumn
private List<Review> reviews;
在这种情况下,集合将被视为 PersistentList
,您将避免重新创建集合。但是,这还需要在 Course_Review
表中添加额外的 order column(必须是整数类型)。当您尝试从列表开头删除项目时,还会进行大量的顺序列更新。
- 您可以使用
Set
接口替换List
(正如 Christian Beikov 所指出的):
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Review> 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.
- When you use the described in your question mapping:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews;
hibernate will treat it as PersistentBag
and the method PersistentCollection.needsRecreate
returns true
(because the BasicCollectionPersister is used).
- You can use the
@OrderColumn
annotation:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@OrderColumn
private List<Review> 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.
- You can use the
Set
interface instead ofList
(as was noticed by Christian Beikov):
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Review> 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论