Отображение полей типа Map встраиваемых (@Embeddable) компонентов с помощью аннотаций @ElementCollection и @CollectionTable

Класс-сущность может содержать поле типа Map, в котором либо ключ, либо значение, либо и то и другое будет встраиваемого (@Embeddable) типа. Такие данные отображаются в отдельные таблицы, которые можно настроить с помощью @ElementCollection и @CollectionTable, а также некоторых других аннотаций.

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

Создадим два встраиваемых класса. Класс Email:

И класс PhoneNumber:

Создадим класс-сущность, в котором встраиваемые классы используются в полях типа Map. В одном случае в качестве ключа, во втором — в качестве значения:

В обоих случаях данные встраиваемых типов будут храниться в отдельных таблицах. Поэтому мы помечаем поля Map<String, Email> emails и Map<PhoneNumber, Boolean> phones аннотациями @ElementCollection и @CollectionTable, а параметре name аннотации @CollectionTable задаём имя этих таблиц.

Что касается поля Map<String, Email> emails, то для полей объектов класса Email колонки будут созданы такими, какими они определены в полях класса Email. А для колонки, в которую будут отображаться данные ключа типа String мы задаём параметры в аннотации @MapKeyColumn. В данном случае в параметр name передаём имя колонки.

У поля Map<PhoneNumber, Boolean> phones встраиваемый тип PhoneNumber является ключом и настройки колонок для данных объектов этого типа определены в классе PhoneNumber. Колонку для значения типа Boolean мы настраиваем в аннотации @Column. В данном случае также в параметр name передаём имя колонки.

Создадим репозиторий для класса-сущности:

Обратите внимание, что в этом выражении использовано два left join fetch, один для таблицы EMAIL, другой для PHONE. Если у вас встраиваемый компонент только один, то соответственно у вас будет только один left join fetch.

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

Тест демонстрирует, что код работает, как и ожидается. Мы сохраняем в базу объект irina с полями emails и phones, которые заполнили подходящими данными. Затем извлекаем из БД по id соответствующую строку и проверяем, что все извлечённые данные всех таблиц полностью соответствуют сохранённым.

Посмотрим, как использованные в тесте данные отобразились в бд:

Действительно для полей типа Map Hibernate создал отдельные таблицы, в которые сохраняет данные как ключа, так и значения каждой записи отображения (Map).

Посмотрим подробнее на таблицы, созданные для встраиваемых классов:

В обоих случаях частью составного первичного ключа Hibernate сделал NOT NULL колонки, в которые отображаются данные из первичных ключей, в комбинации с внешним ключом на родительскую таблицу.

В первом случае в первичных ключ входят данные колонки EMAIL_TYPE, так как в поле Map<String, Email> emails ключом был объект базового типа. Во втором случае, в поле Map<PhoneNumber, Boolean> phones ключом является встраиваемый тип PhoneNumbers и колонки, отображающие данные его полей, и стали компонентами первичного ключа.