Однонаправленное сопоставление @OneToMany удаляет все отношения и повторно добавляет оставшиеся, а не удаляет конкретное

#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 решает, к какому типу из приведенной выше иерархии принадлежит коллекция во время анализа вашей модели домена.

  1. Когда вы используете описанное в вашем вопросе сопоставление:
 @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews;
  

hibernate будет обрабатывать его как PersistentBag и PersistentCollection.needsRecreate возвращает метод true (потому что используется BasicCollectionPersister ).

  1. Вы можете использовать @OrderColumn аннотацию:
 @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@OrderColumn
private List<Review> reviews;
  

в этом случае коллекция будет обрабатываться как the PersistentList , и вы избежите воссоздания коллекции. Но для этого также требуется дополнительный столбец порядка (должен быть целочисленного типа) в Course_Review таблице. И когда вы попытаетесь удалить элемент из начала списка, у вас также будет много обновлений столбцов порядка.

  1. Вы можете использовать Set интерфейс вместо List (как заметил Кристиан Бейков):
 @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Review> reviews;
  

в этом случае коллекция будет обрабатываться как the PersistentSet , и вы также избежите воссоздания коллекции. При использовании наборов очень важно предоставить правильные equals/hashCode реализации для дочерних объектов. Лучшая equals/hashCode реализация, использующая естественный идентификатор или бизнес-ключ. И вы сможете удалить элемент из этой коллекции только по ссылке на объект, поскольку метод remove(int index) просто отсутствует в Set интерфейсе.