Если отображаемый класс содержит поле типа Map, параметризованное базовыми типами (<String, String>, <Integer, Boolean> и т.п.), то данные этого поля не могут быть отображены просто в колонку соответствующей таблицы. Их необходимо отобразить в отдельную таблицу с внешним ключом на id основной.
Например, если класс Person содержит поле Map<String, Boolean> emails, то список адресов электронной почты конкретного человека не может храниться с ним в одной таблице PERSON, нужно завести для них отдельную таблицу EMAILS и хранить там. Hibernate позволяет решить эту задачу, не создавая отдельный класс-сущность Email, а просто разметив поле Map<String, Boolean> emails аннотациями @ElementCollection, @CollectionTable, @MapKeyColumn и @Column.
Подготовка
Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL
Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:
spring.jpa.hibernate.ddl-auto=update
Код
Создадим класс предметной области, содержащий поле типа Map<String, Boolean>:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; @ElementCollection @CollectionTable( name = "EMAILS", joinColumns = @JoinColumn(name = "PERSON_ID") ) @MapKeyColumn(name = "EMAIL") @Column(name = "IS_SUBSCRIBED") private Map<String, Boolean> emails = new HashMap(); //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Мы помечаем поле Map<String, Boolean> emails аннотациями @ElementCollection и @CollectionTable, что указывает Hibernate’у создать для данных этой коллекции отдельную таблицу.
В аннотации @CollectionTable параметр name задаёт имя создаваемой таблицы и является обязательным. Необязательный параметр joinColumns принимает массив аннотаций @JoinColumn, в котором задаются имена колонок таблицы EMAILS, которые будут внешними ключами, ссылающимися на PERSON. Если этот параметр не передать, то в таблице EMAILS будет создана одна колонка-внешний ключ с именем PERSON_ID. Таким образом в нашем примере использование параметра joinColumns избыточно и сделано для демонстрации возможности.
У отображений (Map) есть ключ и значение, каждый из которых отображается в свою колонку в таблице EMAILS. Данные ключа будут отображаться в колонку, имя которой задано параметром name в аннотации @MapKeyColumn, данные значения будут отображены в колонку, заданную аннотацией @Column. В нашем примере в таблице EMAILS появятся две колонки: EMAIL и IS_SUBSCRIBED.
Создадим репозиторий с кастомным запросом, который будет делать выборку из двух таблиц: 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 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Test void mapMapTest() throws Exception { Map<String, Boolean> emails = new HashMap<>() {{ put("irina@mail.ru", true); put("irina@russia.ru", false); }}; Person irina = new Person("Irina", emails); personRepository.save(irina); Person irinaAfterFetching = personRepository.findPersonWithEmails(irina.getId()); assertEquals(irina, irinaAfterFetching); } } |
Тест показывает, что сохранение данных в таблицы и их извлечение происходит как и ожидается.
Посмотрим на схему БД, которую создал Hibernate на основе наших аннотаций:
В схеме БД мы видим, что для поля emails класса Person, как мы и ожидали, была создана отдельная таблица и данные поля emails хранятся именно в ней.
Таблица EMAILS содержит колонку PERSON_ID с внешним ключом, ссылающимся на ID таблицы PERSON, как мы и указывали в параметре joinColumns аннотации @CollectionTable.
Кроме того, данные ключа отображения (Map) — собственно адреса электронной почты — хранятся в колонке email, как мы и указали в аннотации @MapKeyColumn. А данные значения — в колонке IS_SUBSCRIBED, как и было указано в аннотации @Column.
Данные ключа отображения (колонка EMAIL), а также внешний ключ на таблицу PERSON (колонка PERSON_ID) объеденены Hibernate’ом в составной первичный ключ.