问题
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:
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:
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:
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.
来源:https://stackoverflow.com/questions/64363313/unidirectional-onetomany-mapping-deletes-all-relationships-and-re-adds-remainin