При создании двунаправленной связи между двумя классами-сущностями мы должны самостоятельно управлять жизненными циклами объектов каждого класса. Однако, если объекты дочернего класса используются только в полях типа Collection родительского класса, то с помощью настройки orphanRemoval можно указать Hibernate’у удалять из БД строки с данными дочернего класса, при удалении объектов из этой коллекции.
Например, если в классе Person есть поле List и между классами Person и Address установлена двунаправленная связь, то можно настроить автоматическое удаление строк из таблицы Address при удалении соответствующих объектов из List.
Подготовка
Создадим минимальное Spring Boot приложение с поддержкой JPA/Hibernate.
Код
Создадим двунаправленную связь между двумя классами-сущностями. Для этого сперва создадим класс предметной области следующего содержания:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Entity public class Address { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "PERSON_ID", nullable = false) private Person person; @Column(nullable = false) private String city; @Column(nullable = false) private String street; @Column(nullable = false) private String house; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
А затем следующий класс-сущность:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Integer age; @OneToMany(mappedBy = "person", cascade = CascadeType.PERSIST, orphanRemoval = true) private List<Address> addresses; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Подробное описание аннотаций и их параметров, используемых при создании двунаправленной связи, см. в соответствующей статье.
Мы передаём в параметр cascade аннотации @OneToMany значение CascadeType.PERSIST, указывая Hibernate’у автоматически сохранять в БД данные списка addresses при сохранении объекта Person. См. подробней о CascadeType.PERSIST. Без этой настройки автоматическое удаление строк из БД при удалении объектов из списка работать не будет.
Затем мы устанавливаем значение параметра orphanRemoval равным true, указывая Hibernate’у удалять соответствующую строку из таблицы ADDRESS, когда объект будет удаляться из списка addresses.
Создадим репозиторий для класса Address:
1 2 3 4 |
@Repository public interface AddressRepository extends JpaRepository<Address, Long> { List<Address> findByPerson(Person person); } |
Далее создадим репозиторий для класса Person:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Напишем тестовый метод, который продемонстрирует работу настроек:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@SpringBootTest @Transactional class TestApplicationTests { @Autowired PersonRepository personRepository; @Autowired AddressRepository addressRepository; @Test void orphanRemovalTest() { //Создаём объект типа Person и два объекта типа Address. Сохраняем в БД. Person irina = new Person("irina", 28); Address irinasHomeAddress = new Address(irina, "Москва", "Садовая", "12"); Address irinasSummerHouseAddress = new Address(irina, "Воскресенск", "СНТ Строитель", "424"); List<Address> irinasAddresses = new ArrayList<>() {{ add(irinasHomeAddress); add(irinasSummerHouseAddress); }}; irina.setAddresses(irinasAddresses); personRepository.save(irina); //Извлекаем данные из БД Person irinaInDb = personRepository.findById(irina.getId()).get(); List<Address> addressesInDb = addressRepository.findByPerson(irina); //Все адреса сохранились и доступны в БД assertTrue(addressesInDb.containsAll(irinasAddresses)); //Уладяем адрес irinasSummerHouseAddress из списка List<Address> addresses объекта irina и // сохраняем его состояние в БД irinaInDb.getAddresses().remove(irinasSummerHouseAddress); personRepository.save(irinaInDb); //Смотрим, как изменился список адресов в БД addressesInDb = addressRepository.findByPerson(irina); //Данные irinasSummerHouseAddress оказались удалены из БД при удалении их из списка благодаря // настроке orphanRemoval = true assertFalse(addressesInDb.contains(irinasSummerHouseAddress)); } } |
В данном методе мы создаём объект irina Типа Person и список из двух объектов Address. Мы устанавливаем ссылки объектов друг на друга. У каждого объекта Address поле person заполняется ссылкой на объект irina в конструкторе. Затем список Address устанавливается в поле addresses объекта irina через сеттер: irina.setAddresses(irinasAddresses)
.
Затем мы сохраняем только объект irina. Этого достаточно, так как благодаря настройке CascadeType.PERSIST данные объектов irinasHomeAddress и irinasSummerHouseAddress сохранятся при сохранении объекта irina.
Затем мы извлекаем из БД как данные объекта irina, так и два её адреса. Утверждение assertTrue(addressesInDb.containsAll(irinasAddresses))
доказывает, что оба адреса были успешно сохранены и извлечены.
Затем мы удаляем адрес irinasSummerHouseAddress из списка addresses объекта irinaInDb и сохраняем его состояние.
Снова извлекаем адреса из БД. Утверждение assertFalse(addressesInDb.contains(irinasSummerHouseAddress))
доказывает, что адрес irinasSummerHouseAddress был удалён также и из БД. хотя мы только удалили его из списка addresses конкретного объекта. Мы ни разу не вызвали addressRepository.delete(irinasSummerHouseAddress)
. Такое поведение и обусловлено настройкой orphanRemoval = true.
Обратим внимание, что, во-первых, данные объектов Address будут удаляться из БД по одной строке. Таким образом, если их достаточно много, это может сказаться на производительности. Во-вторых, поскольку класс Address объявлен сущностью, в общем случае у его объектов и данных собственный жизненный цикл, который управляется специально созданным для этого репозиторием. Автоматическое удаление данных может привести к тому, что где-то в коде ещё будут оставаться ссылки на удалённые из БД объекты, что приведёт к несогласованности состояния БД и кода.