Установка опции ON DELETE CASCADE на внешний ключ таблицы, ссылающейся на родительскую с помощью настройки OnDeleteAction.CASCADE в Hibernate

Hibernate позволяет настроить на уровне БД каскадное удаление данных дочерних таблиц, при удалении строки родительской таблицы. Поскольку не все СУБД поддерживают данных функционал, то его использование может отразиться на переносимости приложения между разными СУБД.

Подготовка

Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL

Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:

spring.jpa.hibernate.ddl-auto=update

Код

Создадим двунаправленную связь между двумя классами-сущностями. Для этого сперва создадим класс предметной области следующего содержания:

А затем следующий класс-сущность:

Подробное описание аннотаций и их параметров, используемых при создании двунаправленной связи, см. в соответствующей статье.

С помощью аннотации @org.hibernate.annotations.OnDelete и параметра action = org.hibernate.annotations.OnDeleteAction.CASCADE мы указываем Hibernate’у наложить ограничение ON DELETE CASCADE на внешний ключ PERSON_ID таблицы ADDRESS.

Обратим внимание на следующие моменты. Во-первых, аннотацию мы размещаем над полем addresses класса Person, хотя ограничение будет наложено на таблицу ADDRESS. Это необычно, но в этом есть своя логика.

Во-вторых, этот функционал доступен только в Hibernate. Его нет в JPA, поэтому другие провайдеры персистенции его не поддерживают. Хотя, если речь не о легаси, то это скорее всего не проблема.

В-третьих, необходимо, чтобы сама СУБД поддерживала ограничения вида ON DELETE CASCADE.

Создадим репозиторий для класса Address:

Далее создадим репозиторий для класса Person:

Напишем тестовый метод, который продемонстрирует работу настроек:

В данном методе мы создаём объект irina Типа Person и список из двух объектов Address. Мы устанавливаем ссылки объектов друг на друга. У каждого объекта Address поле person заполняется ссылкой на объект irina в конструкторе. Затем список Address устанавливается в поле addresses объекта irina через сеттер: irina.setAddresses(irinasAddresses).

Мы сохраняем в БД как сам объект irina, так и список адресов. Поскольку и Person, и Address объявлены сущностями (@Entity), то мы обязаны сохранять их с помощью их собственных репозиториев, несмотря на наличие связи между ними. Что мы и делаем.

Затем мы извлекаем адреса объекта irina из БД. Утверждение assertTrue(addressesInDb.containsAll(irinasAddresses)) доказывает, что адреса были сохранены и теперь доступны для выбрки.

Затем мы удаляем данные объекта irina из БД и снова пытаемся извлечь данные об адресах. Однако теперь список адресов пуст. Утверждение assertTrue(addressesInDb.isEmpty()) доказывает это. Значит строки таблицы ADDRESS, чей внешний ключ ссылался на id удалённого объекта irina, были удалены автоматически.

Рассмотрим описание таблицы ADDRESS, созданной Hibernate’ом:

У данного подхода есть преимущество перед другими способами автоматического удаления дочерних строк. Каскадное удаление быстрое. В других случаях автоматического удаления Hibernate удаляет строки по одной.

Однако этот подход сохраняет все прочие недостатки автоматического удаления как такового. Во-первых, мы увязываем жизненный цикл одних сущностей с жизненным циклом других. Хотя жизненные циклы сущностей обычно взаимонезависимые (адрес не перестаёт существовать, если кто-то переезжает).

При автоматическом удалении адресов в БД, в коде по прежнему могут остаться ссылки на соответствующие объекты. Это приведёт к несогласованности состояния в БД и в коде.