Автоматическое удаление связанных классов-сущностей при удалении родительского объекта с помощью настройки CascadeType.REMOVE в Hibernate

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

Подготовка

Создадим минимальное Spring Boot приложение с поддержкой JPA/Hibernate.

Код

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

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

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

Мы передаём в параметр cascade аннотации @OneToMany значение CascadeType.REMOVE, указывая Hibernate’у автоматически удалять связанные с объектом Person данные объектов Address при удаление самого объекта Person.

Так как класс Address объявлен сущностью с помощью аннотации @Entity, он имеет собственный репозиторий и объекты этого класса имеют собственный жизненный цикл, который управляется отдельно. Тот факт, что между классами Person и Address есть связь один-ко-многим, ни на что не влияет. Мы должны всякий раз вызывать addressRepository.delete(..), когда хотим удалить из БД данные любого объекта Address.

Однако параметр CascadeType.REMOVE позволяет нам несколько упростить нашу работу, если мы хотим, чтобы Hibernate автоматически удалял данные всех объектов в коллекции addresses из БД, когда мы удаляем из БД сам объект типа Person.

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

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

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

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

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

Затем мы извлекаем адреса из БД, чтобы убедиться, что адреса действительно сохранились.

Затем мы удаляем данные объекта irina из БД.

Затем мы снова делаем запрос на извлечение адресов, связанных с объектом irina, из БД. Утверждение assertTrue(addresses.isEmpty()) доказывает, что извлечённый список оказывается пустым, а значит адреса объекта irina были удалены из базы вместе с его данными.

Таким образом, благодаря параметру CascadeType.REMOVE мы упростили удаление из БД данных дочерних объектов при удалении родительского. Без этой настройки нам пришлось бы это делать вручную.

Однако такой подход создаёт определённую зависимость между жизненными циклами сущностей и должны быть определённые причины, почему сущность Address, признанная собственно сущностью, частично подчиняет свой жизненный цикл сущности Person.

Также обратим внимание, что, во-первых, данные объектов Address будут удаляться из БД по одной строке. Таким образом, если их достаточно много, это может сказаться на производительности. Во-вторых, автоматическое удаление данных может привести к тому, что где-то в коде ещё будут оставаться ссылки на удалённые из БД объекты, что приведёт к несогласованности состояния БД и кода.