Использование отображений (Map) для связи @OneToMany один-ко-многим между сущностями

При создании связи один-ко-многим между сущностями в качестве коллекции, ссылающейся на сущности, которых «много», помимо обычных списков (List) и множеств (Set), также можно использовать отображения (Map). Это позволяет значения любого (обычно уникального) поля вынести в ключ отображения, а в значение положить саму сущность. Что бывает удобно при некоторых обстоятельствах.

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

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

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

Поле типа Person (класс опишем ниже) помечено аннотацией @ManyToOne, что означает, что мы ожидаем, что таблица ADDRESS будет иметь внешний ключ на таблицу PERSON. А с логической точки зрения за одним человеком может быть закреплено несколько адресов.

Сам адрес с нашей точки зрения состоит из названия города (поле city) и остальной части адреса (улица, номер дома и т.д.) в поле location. Мы ожидаем, что поле location будет уникальным, хотя никак это не обозначаем, чтобы не перегружать изложение.

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

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

В аннотации @MapKey мы заполняем параметр name значением «location», что означает, что при формировании отображения Map<String, Address> addresses поле location объектов класса Address будет добавляется в отображение в качестве ключа, а сам объект в качестве значения. Очевидно, что поле типа location должно быть уникальным, чтобы подобная операция имела смысл.

Репозитории

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

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

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

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

Создадим объект irina типа Person и сохраним его данные в БД. Затем создадим список из двух объектов типа Address, связанных с объектам irina, и также сохраним их.

Затем с помощью стримов превратим список полных адресов в список значений полей location. Он нам понадобится в дальнейшем.

Извлечём из БД пользователя «Ирина» по id записи.

Утверждение assertTrue доказывает, что поле Map<String, Address> addresses в качестве списка ключей содержит все те же строки, что созданный выше список List<String> locations. А значит, действительно, Hibernate не просто извлёк из БД список адресов, связанный с объектом irina, но и создал из этого списка отображение, где в качестве ключа использовал поле значение поля location объекта Address, а в качестве значения — сам объект.

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

Так как оба созданных класса являются сущностями, Hibernate создал для них отдельные таблицы.

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

Как мы и ожидали, благодаря аннотации @ManyToOne над полем Person класса Address, таблица ADDRESS содержит колонку PERSON_ID с внешним ключом на идентификатор таблицы PERSON.