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

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

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

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

Мы используем аннотацию @org.hibernate.annotations.Parent, а с ней и поле Person person, чтобы иметь возможность хранить в дочерних объектах ссылку на родительский. Делать это не обязательно, но такая возможность есть.

Создадим класс-сущность следующего содержания:

В данном примере в классе сущности Person у нас список типа Email, где Email — встраиваемый класс-значение. Данные объектов этого класса будут храниться в отдельной таблице. Заметим, что это не является достаточным основанием для того, чтобы Email перестал быть классом значением с аннотацией @Embedded и мы его сделали классом-сущностью с аннотацией @Entity. Да, конкретно в отношениях с классом Person данные электронной почты будут храниться в отдельной таблице. Но другие классы-сущности по-прежнему могут встраивать в себя класс Email и хранить его данные в своих таблицах.

Мы размечаем поле List<Email> emails аннотациями @ElementCollection и @CollectionTable. В параметр name аннотации @CollectionTable мы задаём имя таблицы, в которой будут храниться данные адресов электронной почты, а в параметр joinColumns передаём аннотацию @JoinColumn (можно передать несколько) в которой параметром name задаём имя колонки внешнего ключа, ссылающегося на родительскую строку таблицы PERSON. Если этот параметр не задать, то будет создана колонка с именем ИМЯТАБЛИЦЫ_ID. В нашем примере мы собственно такое имя и задали.

Поскольку у нас поле типа List, а значит мы можем поддерживать сведения о порядке элементов, то мы также добавили аннотацию @OrderColumn с параметром name = «ORD», который задаёт имя колонки, в которой будет храниться порядковый номер строки, соответствующий индексу элемента в списке.

При переопределении метода equals() нужно иметь в виду, что поле List<Email> emails не должно участвовать в определении эквивалентности объектов типа Person. Нужно учесть этот момент при автогенерации equals() для класса Person. При этом эквивалентность дочерних элементов может учитывать поля ссылающиеся на родителя.

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

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

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

Посмотрим на схему данных и сами данные в БД:

И действительно, Hibernate создал для данных объектов класса Email отдельную таблицу. У неё есть колонка PERSON_ID с внешним ключом на родительскую таблицу. А также колонка ORD, где хранится порядок элементов списка.

Поскольку мы создали поле ORD для списка, то Hibernate использует его вместе с внешним ключом PERSON_ID в качестве составного первичного ключа этой вспомогательной таблицы.

А так бы выглядела таблица EMAILS, если бы мы использовали вместо List’а Set:

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