Двунаправленная связь @ManyToMany. Аннотация @JoinTable в Hibernate

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

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

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

Создадим класс предметной области Person следующего содержания:

Для обозначения связи многие-ко-многим мы размечаем поле private List<Address> addresses аннотацией @ManyToMany.

Параметр cascade равный CascadeType.PERSIST указывает Hibernate’у сохранять состояния сущностей списка addresses при сохранении сущности Person, что удобно.

Мы устанавливаем параметр fetch равным FetchType.EAGER после чего Hibernate будет автоматически делать выборку связанных объектов Address при извлеении из БД объекта Person. Значение fetch по умолчанию является LAZY и обычно оно предпочтительней. Но ленивая загрузка связанных сущностей возможно только в рамках транзакции, которые здесь не рассматриваются.

Аннотация @JoinTable является ключевой и позволяет описать таблицу в БД, которая и обеспечит связь многие-ко-многим на уровне БД. Параметр name задаёт имя этой таблицы.

Параметр joinColumns задаёт имя и другие параметры колонок (в joinColumns можно передать массив аннотаций @JoinColumn), которые будут ссылаться на таблицу для текущего класса предметной области (PERSON).

Параметр inverseJoinColumns , соответственно, задаёт имя и другие параметры колонок, которые будут ссылаться на таблицу связанной сущности (ADDRESS).

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

Аналогично классу Person класс Address имеет аннотацию @ManyToMany над полем private List<Person> people. Несмотря на то, что участники связи многие-ко-многим считаются равноправными, тем не менее, мы должны поле одной из сторон в аннотации @ManyToMany передать в параметр mappedBy.

Кроме того, параметр fetch устанавливаем равный FetchType.EAGER, чтобы избежать необходимости использовать транзакционность для ленивой загрузки. При выборке данных любой строки таблицы ADDRESS в объект класса Address, автоматически будут загружены все связанные данные таблицы PERSON.

Репозитории

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

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

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

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

Сперва мы создаём список адресов. Затем создаём объект типа Person и заполняем его поле addresses созданным списком. Благодаря параметру cascade = CascadeType.PERSIST аннотации @ManyToMany мы руками сохраняем в БД только данные объекта Person, а Hibernate в свою очередь также сохранит данные связанных сущностей, а именно двух адресов созданных ранее. Сохранять их вручную нет необходимости.

Затем мы извлекаем из БД сохранённые адреса. Из первого элмента полученного списка мы вычитываем значение первого элемента поля people. Затем ассерт доказывает, что извлечённый из БД таким образом объект типа Person и созданный нами ранее объект irina — эквивалентны.

Данный тест имеет существенный недостаток. Он валиден только на пустых таблицах. Тем не менее он очень просто для восприятия и демонстрирует принцип.

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

Сущности Address и Person получили себе соответствующие таблицы и сиквенсы. Кроме того была создана соединительная таблица person_address, параметры которой мы определили в аннотации @JoinTable.

Рассмотрим схему этой соединительной таблицы:

Как мы и ожидаем от соединительной таблицы, её колонки представляют собой внешние ключи на первичные ключи соединяемых таблиц.