Двунаправленная связь многие-ко-многим через дополнительную сущность в Hibernate

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

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

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

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

Поле List<PersonAddress> personAddress ссылается на объект, который свяжет две сущности, в БД этому объекту будет соответствовать связующая таблица. В аннотацию @OneToMany передаётся mappedBy значение «person», так как в классе PersonAddress на класс Person будет ссылаться поле person (что естественно).

Также мы устанавливаем параметр fetch равным FetchType.EAGER, чтобы связанные данные автоматически загружались из БД вместе с данными таблицы PERSON. По умолчанию предполагается ленивая загрузка этих данных (что обычно предпочтительней), но она требует транзакционности, которую мы сейчас не рассматриваем.

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

Настройки поля List<PersonAddress> peopleAddress, ссылающегося на связующую сущность, полностью аналогичны таковым у класса Person.

Создадим класс-сущность PersonAddress, который обеспечит связь многие-ко-многим между Person и Address. При этом о каждой связи между людьми и адресами (имеются в виду отношения собственности), в поле Boolean isHome будут храниться сведения, является ли адрес домашним для человека; а в поле Integer purchasingYear — дата приобретения объекта недвижимости.

Поля Person person и Address address помечены аннотациями @ManyToOne, что указывает Hibernate’у хранить в них внешние ключи на соответствующие таблицы. Кроме того в аннотациях @JoinColumn мы задаём параметры колонок-внешних ключей. В данном случае мы задаём их имена. Впрочем, те имена, что мы выбрали полностью аналогичны тем, что были бы созданы автоматически, если бы мы ничего не настраивали. Там не менее, мы продемонстрировали возможность дополнительных настроек колонок-внешних ключей.

Репозитории

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

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

А также создадим репозиторий для связующей сущности PersonAddress:

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

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

Создадим два объекта типа Person и сохраним их данные в БД. Далее создадим два объекта Address и также сохраним в БД.

Затем сохраним три связи между объектами Person и Address.

Извлечём из БД данные объекта irina и докажем, что этот объект через промежуточную сущность PersonAddress связан в БД с двумя адресами, как мы это определили выше.

Проведём аналогичную проверку с адресами. Извлечём из БД данные адреса perkovaya. Составим список владельцев этого адреса, доступный через промежуточную сущность PersonAddress. Докажем, что этот список владельцев соответствует списку из объектов irina и vladimir, как мы и определили выше.

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

Поскольку все три класса объявлены сущностями, то Hibernate создал таблицы (а благодаря аннотации @GeneratedValue ещё и сиквенсы) для всех трёх классов.

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

Как мы и ожидаем, соединительная таблица PERSON_ADDRESS содержит две колонки-внешние ключи на идентификаторы таблиц PERSON и ADDRESS. А также дополнительные колонки с данными.