Отображение поля типа Set в отдельную таблицу в Hibernate с помощью аннотаций @ElementCollection и @CollectionTable

Если отображаемый класс содержит поле типа коллекции базового типа, например, Set<String>, то данные этого поля не могут быть отображены просто в колонку соответствующей таблицы. Их необходимо отобразить в отдельную таблицу с внешним ключом на id основной.

Например, если класс Person содержит поле Set<String> emails, то список адресов электронной почты конкретного человека не может храниться с ним в одной таблице PERSON, нужно завести для них отдельную таблицу EMAILS и хранить там. Hibernate позволяет решить эту задачу, не создавая отдельный класс-сущность Email, а просто разметив поле Set<String> emails аннотациями @ElementCollection и @CollectionTable.

Так как в данном случае речь идёт о поле типа Set, то элементы будут считаться не упорядоченными. Если необходимо поддерживать порядок элементов, то поле должно иметь тип List.

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

Создадим класс предметной области, содержащий поле типа Set<String>:

Мы помечаем поле Set<String> emails аннотациями @ElementCollection и @CollectionTable, что указывает Hibernate’у создать для данных этой коллекции отдельную таблицу.

В аннотации @CollectionTable параметр name задаёт имя создаваемой таблицы и является обязательным. Необязательный параметр joinColumns принимает массив аннотаций @JoinColumn, в котором задаются имена колонок таблицы EMAILS, которые будут внешними ключами, ссылающимися на PERSON. Если этот параметр не передать, то в таблице EMAILS будет создана одна колонка-внешний ключ с именем PERSON_ID. Таким образом в нашем примере использование параметра joinColumns избыточно и сделано для демонстрации возможности.

Создадим репозиторий с кастомным запросом, который будет делать выборку из двух таблиц: PERSON и из связанной с ней таблицы EMAILS:

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

Тест показывает, что сохранение данных в таблицы и их извлечение происходит как и ожидается.

Посмотрим на схему БД, которую создал Hibernate на основе наших аннотаций:

В схеме БД мы видим, что для поля emails класса Person, как мы и ожидали, была создана отдельная таблица и данные поля emails хранятся именно в ней.

Таблица EMAILS содержит колонку PERSON_ID с внешним ключом, ссылающимся на ID таблицы PERSON, как мы и указывали в параметре joinColumns аннотации @CollectionTable.