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

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

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

Так как в данном случае речь идёт о поле типа List, то элементы будут считаться упорядоченными, что позволит с помощью аннотации @OrderColumn задать отдельную колонку для хранения данных о порядке элементов.

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

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

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

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

Кроме того, мы помечаем поле emails аннотацией @OrderColumn, что указывает Hibernate’у создать в таблице EMAILS специальную колонку, в которой будет храниться порядок следования элементов. Имя колонки можно задать параметром name этой аннотации. В нашем случае это ORD. Если имя не задать, то колонка будет названа по схеме ИМЯТАБЛИЦЫ_ORDER.

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

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

В данном тесте мы добавляем в список email переменной irina два адреса электронной почты: «irina@first.ru» и «irina@third.ru» и сохраняем данные в БД. Затем мы вставляем во вторую позицию в списке адрес «irina@second.ru» и снова сохраняем данные. Таким образом с точки зрения порядка вставки адресов в БД мы вставили первый, третий и в конце — второй. С точки зрения порядка адресов в списке — они расположены по порядку первый, второй, третий.

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

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

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

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

Кроме того, таблица EMAILS содержит колонку ORD, которую мы задали аннотацией @OrderColumn, в которой хранится порядок следования элементов списка. Изменив порядок элементов в списке List<String> emails и сохранив объект, мы изменим данные этой колонки.

Колонка ORD вместе с внешним ключом PERSON_ID объеденены Hibernate’ом в составной первичный ключ.