Если класс-сущность содержит не просто поле встраиваемого (@Embeddable) типа, а коллекцию объектов встраиваемого типа, то для данных этих объектов будет создана отдельная таблица, которая настраивается с помощью аннотаций @ElementCollection и @CollectionTable.
Подготовка
Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL
Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:
spring.jpa.hibernate.ddl-auto=update
Код
Создадим встраиваемый класс следующего содержания:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Embeddable public class Email { @org.hibernate.annotations.Parent private Person person; @Column(nullable = false) private String name; @Column(nullable = false) private String domain; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Мы используем аннотацию @org.hibernate.annotations.Parent, а с ней и поле Person person, чтобы иметь возможность хранить в дочерних объектах ссылку на родительский. Делать это не обязательно, но такая возможность есть.
Создадим класс-сущность следующего содержания:
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") private List<Email> emails = new ArrayList<>(); //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
В данном примере в классе сущности 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. При этом эквивалентность дочерних элементов может учитывать поля ссылающиеся на родителя.
Создадим репозиторий для класса-сущности:
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 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Test void mapListOfEmbeddablesTest() throws Exception { Person irina = new Person("Irina"); List<Email> emails = List.of( new Email(irina, "irina", "mail.ru"), new Email(irina, "irina", "russia.ru") ); irina.setEmails(emails); personRepository.save(irina); Person irinaAfterFetching = personRepository.findPersonWithEmails(irina.getId()); assertEquals(irina, irinaAfterFetching); assertEquals(emails, irinaAfterFetching.getEmails()); } } |
Тест демонстрирует, что код работает, как и ожидается. Мы сохраняем в базу объект irina с полем List<Email> emails, в который поместили два объекта. Затем извлекаем из БД по id соответствующую строку и проверяем, что извлечённые данные полностью соответствуют сохранённым.
Посмотрим на схему данных и сами данные в БД:
И действительно, Hibernate создал для данных объектов класса Email отдельную таблицу. У неё есть колонка PERSON_ID с внешним ключом на родительскую таблицу. А также колонка ORD, где хранится порядок элементов списка.
Поскольку мы создали поле ORD для списка, то Hibernate использует его вместе с внешним ключом PERSON_ID в качестве составного первичного ключа этой вспомогательной таблицы.
А так бы выглядела таблица EMAILS, если бы мы использовали вместо List’а Set:
В случае, если бы мы использовали коллекцию типа Set, то благодаря тому, что все колонки объявлены NOT NULL, Hibernate использовал их в сочетании с внешним ключом в качестве составного первичного ключа.