Создание связи один-ко-многим является типичной задачей. Главным отличием описания такой связи в 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. Класс описан таким образом, будто адрес электронной почты никак не связан с пользователем.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Entity public class Email { @Id @GeneratedValue private Long id; private String name; private String domain; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Создадим класс предметной области Person, в котором организуем связь типа один-ко-многим между Person и Email:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; @OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "PERSON_ID") private List<Email> emails = new ArrayList<>(); //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Класс Person, напротив, содержит список объектов типа Email. Но в таблицах в БД мы ожидаем прямо противоположную картину, что строки таблицы EMAIL будут ссылаться на строки таблицы PERSON.
Репозитории
Создадим репозиторий для класса Person:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Также создадим репозиторий для класса Email:
1 2 3 |
@Repository public interface EmailRepository extends JpaRepository<Email, 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 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Autowired EmailRepository emailRepository; @Test void oneToManyUnidirectionalTest() { List<Email> emails = List.of( new Email("irina", "russia.ru"), new Email("irina", "mail.ru") ); emailRepository.saveAll(emails); // insert into email (domain, name, id) values (?, ?, ?) // insert into email (domain, name, id) values (?, ?, ?) Person irina = new Person("Irina"); irina.setEmails(emails); personRepository.save(irina); // insert into person (name, id) values (?, ?) // update email set person_id=? where id=? // update email set person_id=? where id=? Person irinaInDb = personRepository.findById(irina.getId()).get(); assertEquals(emails, irinaInDb.getEmails()); } } |
Сперва мы создаём список объектов типа 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.