Класс-сущность может содержать поле типа Map, в котором либо ключ, либо значение, либо и то и другое будет встраиваемого (@Embeddable) типа. Такие данные отображаются в отдельные таблицы, которые можно настроить с помощью @ElementCollection и @CollectionTable, а также некоторых других аннотаций.
Подготовка
Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL
Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:
spring.jpa.hibernate.ddl-auto=update
Код
Создадим два встраиваемых класса. Класс Email:
1 2 3 4 5 6 7 8 9 10 11 |
@Embeddable public class Email { @Column(nullable = false) private String name; @Column(nullable = false) private String domain; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
И класс PhoneNumber:
1 2 3 4 5 6 7 8 9 10 11 |
@Embeddable public class PhoneNumber { @Column(nullable = false) private String countryCode; @Column(nullable = false) private String number; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Создадим класс-сущность, в котором встраиваемые классы используются в полях типа Map. В одном случае в качестве ключа, во втором — в качестве значения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; @ElementCollection @CollectionTable(name = "EMAIL") @MapKeyColumn(name = "EMAIL_TYPE") private Map<String, Email> emails = new HashMap(); @ElementCollection @CollectionTable(name = "PHONE") @Column(name = "IS_DOMESTIC") private Map<PhoneNumber, Boolean> phones; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
В обоих случаях данные встраиваемых типов будут храниться в отдельных таблицах. Поэтому мы помечаем поля Map<String, Email> emails и Map<PhoneNumber, Boolean> phones аннотациями @ElementCollection и @CollectionTable, а параметре name аннотации @CollectionTable задаём имя этих таблиц.
Что касается поля Map<String, Email> emails, то для полей объектов класса Email колонки будут созданы такими, какими они определены в полях класса Email. А для колонки, в которую будут отображаться данные ключа типа String мы задаём параметры в аннотации @MapKeyColumn. В данном случае в параметр name передаём имя колонки.
У поля Map<PhoneNumber, Boolean> phones встраиваемый тип PhoneNumber является ключом и настройки колонок для данных объектов этого типа определены в классе PhoneNumber. Колонку для значения типа Boolean мы настраиваем в аннотации @Column. В данном случае также в параметр name передаём имя колонки.
Создадим репозиторий для класса-сущности:
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 left join fetch p.phones where p.id = :id") Person findPersonWithEmails(@Param("id") Long id); } |
Обратите внимание, что в этом выражении использовано два left join fetch
, один для таблицы EMAIL, другой для PHONE. Если у вас встраиваемый компонент только один, то соответственно у вас будет только один left join fetch
.
Напишем тест, который продемонстрирует работу кода:
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 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Test void mapEmbeddedMapTest() throws Exception { Map<String, Email> emails = new HashMap<>() {{ put("personal", new Email("irina", "mail.ru")); put("work", new Email("irina", "company.ru")); }}; Map<PhoneNumber, Boolean> phones = new HashMap<>() {{ put(new PhoneNumber("+7", "123 456 78 90"), true); put(new PhoneNumber("+84", "123 456 78 90"), false); }}; Person irina = new Person("Irina", emails, phones); personRepository.save(irina); Person irinaAfterFetching = personRepository.findPersonWithEmails(irina.getId()); assertEquals(irina, irinaAfterFetching); assertEquals(emails, irinaAfterFetching.getEmails()); assertEquals(phones, irinaAfterFetching.getPhones()); } } |
Тест демонстрирует, что код работает, как и ожидается. Мы сохраняем в базу объект irina с полями emails и phones, которые заполнили подходящими данными. Затем извлекаем из БД по id соответствующую строку и проверяем, что все извлечённые данные всех таблиц полностью соответствуют сохранённым.
Посмотрим, как использованные в тесте данные отобразились в бд:
Действительно для полей типа Map Hibernate создал отдельные таблицы, в которые сохраняет данные как ключа, так и значения каждой записи отображения (Map).
Посмотрим подробнее на таблицы, созданные для встраиваемых классов:
В обоих случаях частью составного первичного ключа Hibernate сделал NOT NULL колонки, в которые отображаются данные из первичных ключей, в комбинации с внешним ключом на родительскую таблицу.
В первом случае в первичных ключ входят данные колонки EMAIL_TYPE, так как в поле Map<String, Email> emails ключом был объект базового типа. Во втором случае, в поле Map<PhoneNumber, Boolean> phones ключом является встраиваемый тип PhoneNumbers и колонки, отображающие данные его полей, и стали компонентами первичного ключа.