Глава 23. Пример. Родитель/Ребёнок

Оглавление
  1. 23.1. Заметка о коллекциях
  2. 23.2. Двунаправленный «один-ко-многим»
  3. 23.3. Каскадный жизненный цикл
  4. 23.4. Каскады и unsaved-value (несохраненные значения)
  5. 23.5. Вывод

Одной из первых вещей, которые новые пользователи хотят делать с Hibernate, является модель отношения типа родитель/ребёнок. Есть два разных подхода к этому. Наиболее удобный подход, особенно для новых пользователей, заключается в моделировании как Parent, так и Child в качестве классов сущностей с ассоциацией <one-to-many> от Parent к Child. Альтернативный подход заключается в объявлении Child как <composite-element>. В Hibernate семантика по умолчанию для ассоциации «один-ко-многим» гораздо менее близка к обычной семантике отношения родитель/ребёнок, чем к отображению составных элементов. Мы объясним, как использовать двунаправленную связь «один-ко-многим» с каскадами для эффективного и элегантного моделирования отношений родитель/ребёнок.

23.1. Заметка о коллекциях

Коллекции Hibernate считаются логической частью их собственной сущности, а не содержащихся в ней сущностей. Имейте в виду, что это критическое различие, которое имеет следующие последствия:

Добавление сущности в коллекцию по умолчанию просто создает связь между двумя сущностями. Удаление объекта удалит ссылку. Это подходит для всех случаев. Однако это не уместно в случае отношения родитель/ребёнок. В этом случае жизнь ребёнка связана с жизненным циклом родителя.

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:

Это не только неэффективно, но и нарушает ограничение 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. К сожалению, существуют два больших ограничения для элементов составных классов: составные элементы не могут иметь собственные коллекции, и они не должны быть дочерними элементами какой-либо сущности, кроме уникального родителя.