Встраиваемые(@Embeddable) классы-значения могут содержать поле типа той или иной коллекции (List, Set и т.п.). Тогда данные этой коллекции будут храниться в отдельной таблице, которая может быть настроена с помощью аннотаций @ElementCollection и @CollectionTable.
Подготовка
Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL
Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:
spring.jpa.hibernate.ddl-auto=update
Код
Создадим встраиваемый класс-значение, содержащий одно поле типа Set:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Embeddable public class Email { @Column(name = "EMAIL_USERNAME", nullable = false) private String name; @Column(name = "EMAIL_DOMAIN", nullable = false) private String domain; @ElementCollection @CollectionTable( name = "PERMISSION", joinColumns = @JoinColumn(name = "EMAIL_ID") ) @Column(name = "EMAIL_PERMISSIONS", nullable = false) private Set<String> permissions = new HashSet<>(); //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Мы помечаем поле Set<String> аннотациями @ElementCollection и @CollectionTable. Данные этого поля будут храниться в отдельной таблице. В параметрах аннотации @CollectionTable мы задаём настройки этой таблицы. В параметр name передаём название этой таблицы. В параметре joinColumns мы передаём аннотацию @JoinColumn (можно передать несколько массивом), в которой в параметре name задаём имя колонки-внешнего ключа на родительскую таблицу. Если её не задать, то будет автоматически создана колонка вида ИМЯТАБЛИЦЫ_ID. Но мы именно такое имя и задали, только вручную.
Напишем класс-сущность, содержащий поле созданного ранее класс-значения:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Email email; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Мы ожидаем, что раз поле email имеет тип Email, который является встраиваемым типом (помечен аннотацией @Embeddable), то поля объектов классов Person и Email будут отображены в одну таблицу 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.email.permissions where p.id = :id") Person findPersonWithEmails(@Param("id") Long id); } |
Обратите внимание, как мы присоединяем данные из таблицы PERMISSIONS. У класса Person нет прямой ссылки на Set<String> permissions, поэтому ссылка получается ступенчатая: p.email.permissions
.
Напишем тест, который продемонстрирует работу кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Test void mapEmbeddedWithCollection() throws Exception { Email irinasEmail = new Email( "irina", "mail.ru", Set.of("notifications", "personal offers", "public offers") ); Person irina = new Person("Irina", irinasEmail); personRepository.save(irina); Person irinaAfterFetching = personRepository.findPersonWithEmails(irina.getId()); assertEquals(irina, irinaAfterFetching); assertEquals(irinasEmail.getPermissions(), irinaAfterFetching.getEmail().getPermissions()); } } |
Тест демонстрирует, что код работает, как и ожидается. Мы сохраняем в базу объект irina с полями email, которое в свою очередь содержит множество (Set) строк с «разрешениями». Затем извлекаем из БД по id соответствующую строку и проверяем, что все извлечённые данные полностью соответствуют сохранённым.
Рассмотри созданную схему данных:
Мы видим, что, действительно, Hibernate создал для данных поля Set<String> permission отдельную таблицу, тогда как данные остальных полей встраиваемого класса Email сохраняет в таблицу PERSON, как мы и ожидаем от полей встраиваемых классов.