Если отображаемый класс содержит поле типа 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>:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; @ElementCollection @CollectionTable( name = "EMAILS", joinColumns = @JoinColumn(name = "PERSON_ID") ) @OrderColumn(name = "ORD") @Column(name = "EMAIL") private List<String> emails = new ArrayList<>(); //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Мы помечаем поле 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:
1 2 3 4 5 6 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { @Query("select p from Person p left join fetch p.emails where p.id = :id") Person findPersonWithEmails(@Param("id") Long id); } |
Напишем тест, который продемонстрирует работу нашего кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Test void mapListWithOrderTest() throws Exception { String firstEmail = "irina@first.ru"; String secondEmail = "irina@second.ru"; String thirdEmail = "irina@third.ru"; Person irina = new Person("Irina"); irina.getEmails().add(firstEmail); irina.getEmails().add(thirdEmail); personRepository.save(irina); irina.getEmails().add(1, secondEmail); personRepository.save(irina); Person irinaAfterFetching = personRepository.findPersonWithEmails(irina.getId()); assertEquals(irina, irinaAfterFetching); assertEquals(firstEmail, irinaAfterFetching.getEmails().get(0)); assertEquals(secondEmail, irinaAfterFetching.getEmails().get(1)); assertEquals(thirdEmail, irinaAfterFetching.getEmails().get(2)); } } |
В данном тесте мы добавляем в список 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’ом в составной первичный ключ.