Глава 23. Пример. Родитель/Ребёнок
Оглавление- 23.1. Заметка о коллекциях
- 23.2. Двунаправленный «один-ко-многим»
- 23.3. Каскадный жизненный цикл
-
23.4. Каскады и
unsaved-value
(несохраненные значения) - 23.5. Вывод
Одной из первых вещей, которые новые пользователи хотят делать с Hibernate, является модель
отношения типа родитель/ребёнок. Есть два разных подхода к этому. Наиболее удобный подход, особенно
для новых пользователей, заключается в моделировании как Parent
, так
и Child
в качестве классов сущностей с ассоциацией <one-to-many>
от Parent
к Child
. Альтернативный подход заключается в объявлении
Child
как <composite-element>. В Hibernate семантика по умолчанию
для ассоциации «один-ко-многим» гораздо менее близка к обычной семантике отношения
родитель/ребёнок, чем к отображению составных элементов. Мы объясним, как использовать
двунаправленную связь «один-ко-многим» с каскадами для эффективного и элегантного
моделирования отношений родитель/ребёнок.
23.1. Заметка о коллекциях
Коллекции Hibernate считаются логической частью их собственной сущности, а не содержащихся в ней сущностей. Имейте в виду, что это критическое различие, которое имеет следующие последствия:
- Когда вы удаляете/добавляете объект из/в коллекцию, номер версии владельца коллекции увеличивается.
- Если объект, который был удален из коллекции, является экземпляром типа значения (value type) (например, составного элемента), этот объект перестанет быть постоянным (persistent), и его состояние будет полностью удалено из базы данных. Аналогично, добавление экземпляра типа значения в коллекцию приведет к тому, что его состояние станет постоянным немедленно.
- И наоборот, если сущность удаляется из коллекции (ассоциация «один-ко-многим» или «многие-ко-многим»), она по умолчанию не будет удалена. Такое поведение полностью согласуется; изменение внутреннего состояния другой сущности не должно приводить к исчезновению связанной сущности. Аналогично, добавление сущности в коллекцию не приводит к тому, что эта сущность становится постоянной (persistent) по умолчанию.
Добавление сущности в коллекцию по умолчанию просто создает связь между двумя сущностями. Удаление объекта удалит ссылку. Это подходит для всех случаев. Однако это не уместно в случае отношения родитель/ребёнок. В этом случае жизнь ребёнка связана с жизненным циклом родителя.
23.2. Двунаправленный «один-ко-многим»
Предположим, что мы начинаем с простой ассоциации <one-to-many> от Parent
к Child
.
<set name="children"> <key column="parent_id" /> <one-to-many class="Child" /> </set>
Если бы мы выполнили следующий код:
Parent p = .....; Child c = new Child(); p.getChildren().add(c); session.save(c); session.flush();
Hibernate выдаст два оператора SQL:
-
INSERT
, чтобы создать запись дляc
-
UPDATE
для создания ссылки отp
кc
Это не только неэффективно, но и нарушает ограничение NOT NULL
в столбце
parent_id
. Вы можете исправить нарушение ограничения допустимости null, указав
в отображении коллекции not-null="true"
:
<set name="children"> <key column="parent_id" not-null="true" /> <one-to-many class="Child" /> </set>
Однако это не рекомендуемое решение.
Основной причиной такого поведения является то, что ссылка (внешний ключ parent_id
)
от p
к c
не считается частью состояния дочернего объекта
и поэтому не создается в INSERT
. Решение состоит в том, чтобы сделать ссылку
частью отображение Child
.
<many-to-one name="parent" column="parent_id" not-null="true"/>
Вам также необходимо добавить parent
свойство в класс Child
.
Теперь, когда дочерняя сущность управляет состоянием ссылки, мы сообщаем коллекции не обновлять
ссылку. Для этого мы используем атрибут inverse
:
<set name="children" inverse="true"> <key column="parent_id" /> <one-to-many class="Child" /> </set>
Следующий код будет использоваться для добавления нового Child
:
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); c.setParent(p); p.getChildren().add(c); session.save(c); session.flush();
Теперь будет выпущен только один SQL INSERT
.
Вы также можете создать метод addChild()
для Parent
.
public void addChild(Child c) { c.setParent(this); children.add(c); }
Код для добавления Child
выглядит следующим образом:
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); p.addChild(c); session.save(c); session.flush();
23.3. Каскадный жизненный цикл
Вы можете избавиться от разочарований явного вызова save()
с помощью каскадов.
<set name="children" inverse="true" cascade="all"> <key column="parent_id" /> <one-to-many class="Child" /> </set>
Это упрощает приведенный выше код:
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); p.addChild(c); session.flush();
Аналогичным образом, при сохранении или удалении Parent
нам не нужно перебирать детей.
Следующий код удаляет Parent
и все его дочерние элементы из базы данных.
Parent p = (Parent) session.load(Parent.class, pid); session.delete(p); session.flush();
Однако следующий код:
Parent p = (Parent) session.load(Parent.class, pid); Child c = (Child) p.getChildren().iterator().next(); p.getChildren().remove(c); c.setParent(null); session.flush();
не удаляет Child
из базы данных. В этом случае он удалит только ссылку
на Parent
и вызовет нарушение ограничения NOT NULL
.
Вам нужно явно вызвать delete()
для Child.
Parent p = (Parent) session.load(Parent.class, pid); Child c = (Child) p.getChildren().iterator().next(); p.getChildren().remove(c); session.delete(c); session.flush();
В нашем случае Child
не может существовать без Parent
. Поэтому, если
мы удалим Child
из коллекции, мы хотим, чтобы он был удалён вообще.
Для этого мы должны использовать cascade="all-delete-orphan"
.
<set name="children" inverse="true" cascade="all-delete-orphan"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
Несмотря на то, что в отображении коллекции указано inverse="true"
,
каскады все еще обрабатываются путем итерации элементов коллекции. Если вам нужен объект, который будет
сохранён, удалён или обновлён каскадом, вы должны добавить его в коллекцию. Недостаточно просто
вызвать setParent()
.
23.4. Каскады и unsaved-value
(несохраненные значения)
Предположим, мы загрузили Parent
за один Session
, внесли некоторые
изменения в действие пользовательского интерфейса и захотели сохранить эти изменения
в новом сессии, вызвав update()
. Parent
будет содержать коллекцию детей,
и, поскольку каскадное обновление включено, Hibernate должен знать, какие дочерние элементы были недавно
созданы и которые представляют собой существующие строки в базе данных. Мы также предположим,
что оба и родителя
и Child
генерировали свойства идентификатора типа
Long
. Hibernate будет использовать идентификатор и значение свойства version/timestamp,
чтобы определить, кто из этих новый. (См. Раздел
11.7 «Автоматическое обнаружение состояния»).
В Hibernate больше нет необходимости явно указывать unsaved-value
(несохранённое значение).
Следующий код обновит parent
и child
и вставит newChild
:
// Parent и Child были загружены в предыдущей сессии parent.addChild(child); Child newChild = new Child(); parent.addChild(newChild); session.update(parent); session.flush();
Это может быть подходящим для случая когда идентификатор сгенерирован, но как насчёт присвоеных идентификаторов и составных идентификаторов? Это сложнее, так как Hibernate не может использовать свойство идентификатора для различия вновь созданного объекта с идентификатором, назначенным пользователем, и объектом, загруженным в предыдущей сессии. В этом случае Hibernate будет либо использовать свойство timestamp или version, либо будет запрашивать кэш второго уровня или, в худшем случае, базу данных, чтобы увидеть, существует ли строка.
23.5. Вывод
Разделы, которые мы только что рассмотрели, могут показаться немного запутанными. Однако на практике все это хорошо работает. Большинство приложений Hibernate используют шаблон parent/child во многих случаях.
Мы упоминали об альтернативе в первом абзаце. Ни одна из вышеперечисленных проблем не существует в случае отображений <composite-element>, которые имеют точную семантику отношения parent/child. К сожалению, существуют два больших ограничения для элементов составных классов: составные элементы не могут иметь собственные коллекции, и они не должны быть дочерними элементами какой-либо сущности, кроме уникального родителя.