Однонаправленная связь @OneToMany между сущностями в Hibernate

Создание связи один-ко-многим является типичной задачей. Главным отличием описания такой связи в JPA/Hibernate от описания в БД заключается в том, что если в БД мы всегда описываем связь в ссылающейся таблице, то в Hibernate — это можно сделать в той сущности, на которую ссылаются.

Например, допустим у нас есть человек и у него есть адреса электронной почты. Тогда в БД в таблице EMAIL мы опишем колонку с внешним ключом на таблицу PERSON. Hibernate же позволяет в классе Person создать поле List<Email> emails и, аннотировав его должным образом, описать аналогичную связь. Класс Email же будет описан таким образом, как будто бы он вообще никак не связан с классом Person. Хотя в БД мы по-прежнему получим таблицу EMAIL с внешним ключом на PERSON.

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Также добавим в application.properties настройку, позволяющую видеть создаваемый Hibernate’ом SQL в консоли:

spring.jpa.show-sql=true

Код

Классы предметной области

Создадим класс предметной области Email. Класс описан таким образом, будто адрес электронной почты никак не связан с пользователем.

Создадим класс предметной области Person, в котором организуем связь типа один-ко-многим между Person и Email:

Класс Person, напротив, содержит список объектов типа Email. Но в таблицах в БД мы ожидаем прямо противоположную картину, что строки таблицы EMAIL будут ссылаться на строки таблицы PERSON.

Репозитории

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

Также создадим репозиторий для класса Email:

Проверка кода

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

Сперва мы создаём список объектов типа Email и сохраняем их данные в БД. Поскольку Email является сущностью, то есть имеет собственный жизненный цикл, то мы не делали связь между Person и Email обязательной. Данные об адресах электронной почты хранятся независимо от того, знаем ли мы, с кем из пользователей связан конкретный адрес. Таким образом только что сохранённые два адреса никак не связаны ни с каким Person.

Затем мы создаём объект irina типа Person и заполняем поле List<Email> emails только что созданными адресами. Таким образом мы установим в коде связь один-ко-многим между одним объектом Person и двумя объектами Email.

Затем мы сохраняем объект irina в БД. Помимо собственно вставки в таблицу PERSON мы видим, что Hibernate также создаёт в БД связь между строками таблицы EMAIL и строкой в таблице PERSON: update email set person_id=? where id=?. Собственно таким образом произошло отображение связи между сущностями в БД.

Далее мы проверяем, что если мы извлечём строку из таблицы PERSON, а с ней и зависимые сущности из EMAIL, то это будут именно те данные, которые мы сохранили ранее.

Рассмотрим состояние таблиц БД после выполнения выполнения кода:

Как мы и ожидали обе сущности имеют собственные таблицы и собственные сиквенсы для них. Хотя в классе Email никак не описана связь с классом Person и, соответственно, в классе нет никакого поля personId. Тем не менее, Hiebrnate создал колонку PERSON_ID в таблице EMAIL для того, чтобы связь между сущностями могла существовать в БД.

Рассмотрим схему таблиц, созданную Hibernate’ом:

Как мы видим, связь один-ко-многим между Person и Email, будучи в коде описанной в классе Person, в схеме БД, как и положено, реализована через внешний ключ в таблице EMAIL.