#java #hibernate #jpa #one-to-many #hibernate-mapping
#java #спящий режим #jpa #один ко многим #отображение в спящий режим
Вопрос:
Учитывая следующий код
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]
Обратите внимание, что сначала он удаляет все отношения, а затем вставляет оставшиеся. Почему такое поведение? Почему это не могло быть более конкретным и удалить только одну запись, хранящую отношения.
Ответ №1:
Не уверен, связано ли это с семантикой пакета (потому что вы используете a List
, а не Set
для обзоров) или просто потому, что Hibernate иногда выполняет так называемые «воссоздания коллекции». Попробуйте использовать Set
.
Комментарии:
1. Спасибо! это сработало. Но почему он показывает такое разное поведение для Set и List, хотя действие совершенно одинаковое. Можете ли вы указать мне на какую-нибудь статью, где я могу прочитать об этом воссоздании коллекции
2. Проблема в том, что когда у вас есть семантика пакета, т.Е. «Несколько возможных строк с одинаковыми значениями», вы не можете просто выполнить оператор delete для удаления «первого» элемента. Поэтому Hibernate должен удалить все элементы, а затем снова вставить их.
Ответ №2:
Hibernate делает это, потому что он понятия не имеет о том, как связаны сущности. Поскольку нет информации о том, как идентифицируются отношения, он использует единственную имеющуюся у него информацию — объекты в памяти. Таким образом, он очищает таблицу с помощью предиката и сохраняет объекты из памяти.
Вам нужно использовать @JoinColumn
на дочерней стороне и mappedBy
параметр @OneToMany
на родительской стороне.
Комментарии:
1. Но объекты действительно содержат информацию. Как у курса, так и у обзора, который был удален, идентификаторы заполняются в itd. Идентификатор reviews_id, который требуется для уточнения в запросе на удаление «удалить из course_reviews, где course_id=?», Как известно, уже находится в спящем режиме во время удаления, не понимаю, почему hibernate не может это выяснить.
2. @KP- нет никакой информации вообще. Вам нужно понять, как работает отображение в режиме гибернации. Взгляните на эту статью: vladmihalcea.com /…
3. Спасибо за ответ. Но если я просто изменю его на Set вместо List, он запускает более конкретный запрос «удалить из course_reviews, где course_id=? и reviews_id =?»
4. @KP- нет проблем. Конечно, это простое решение. Однако вскоре у вас возникнут проблемы с производительностью;)
5. Этот способ сопоставления определенно не является правильным способом сопоставления «один ко многим», и я согласен с вашими комментариями. Но вопрос, который у меня был, был более конкретно об удалении и повторном добавлении отношений 🙂
Ответ №3:
Прежде всего, поведение, которое вы видите, описано в документации:
Однонаправленные ассоциации не очень эффективны, когда дело доходит до удаления дочерних объектов. В приведенном выше примере после очистки контекста сохранения Hibernate удаляет все строки базы данных из таблицы ссылок (например, Person_Phone), которые связаны с родительским объектом Person, и повторно вставляет те, которые все еще находятся в
@OneToMany
коллекции.С другой стороны, двунаправленная
@OneToMany
ассоциация намного эффективнее, поскольку дочерняя сущность управляет ассоциацией.
Что касается вопроса:
Почему такое поведение? Почему это не могло быть более конкретным и удалить только одну запись, хранящую отношения.
Ответ не так прост и требует глубокого погружения в исходный код гибернации.
Ключевым моментом обработки коллекции объекта в режиме гибернации является интерфейс PersistentCollection . Как указано в комментариях к этому интерфейсу:
Hibernate переносит коллекцию Java в экземпляр
PersistentCollection
. Этот механизм предназначен для поддержки отслеживания изменений постоянного состояния коллекции и отложенного создания экземпляров элементов коллекции. Недостатком является то, что поддерживаются только определенные абстрактные типы коллекций, и любая дополнительная семантика теряется.
Важное место в нашем обсуждении занимает следующий метод этого интерфейса:
/**
* 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 проверяет необходимость восстановления коллекции на основе on the collection.needsRecreate(persister)
call .
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;
в этом случае коллекция будет обрабатываться как the PersistentList
, и вы избежите воссоздания коллекции. Но для этого также требуется дополнительный столбец порядка (должен быть целочисленного типа) в Course_Review
таблице. И когда вы попытаетесь удалить элемент из начала списка, у вас также будет много обновлений столбцов порядка.
- Вы можете использовать
Set
интерфейс вместоList
(как заметил Кристиан Бейков):
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Review> reviews;
в этом случае коллекция будет обрабатываться как the PersistentSet
, и вы также избежите воссоздания коллекции. При использовании наборов очень важно предоставить правильные equals/hashCode
реализации для дочерних объектов. Лучшая equals/hashCode
реализация, использующая естественный идентификатор или бизнес-ключ. И вы сможете удалить элемент из этой коллекции только по ссылке на объект, поскольку метод remove(int index)
просто отсутствует в Set
интерфейсе.