В объектно-реляционном отображении различают сущности (entity) и значения (value). Сущности — это данные, имеющие свой жизненный цикл, а с ним и собственную таблицу с id в БД. Значения не имеют собственного жизненного цикла и, как правило, сохраняются в одну строку с сущностью, от которой зависят.
Так, например, адрес может с одной стороны быть представлен отдельным классом в Java, но в БД храниться в виде соответствующих колонок в таблице PERSON. Если адрес у каждого человека один и существует только в привязке к конкретному человеку, то нет никакой необходимости заводить для адресов отдельную таблицу ADDRESS и делать связь один к одному между таблицами PERSON и ADDRESS. Достаточно хранить все данные в PERSON.
Чтобы при этом было возможно в Java-коде пользоваться для адресов отдельным классом, мы можем использовать аннотацию @Embeddable над классом Address.
Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL
Создадим класс для данных-значений следующего содержания:
1 2 3 4 5 6 7 8 9 10 11 |
@Embeddable public class Address { @Column(length = 100) private String city; private String street; private String details; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Аннотация @Embeddable указывает, что мы планируем сохранять данные полей объектов этого класса в колонки таблиц, создаваемых для других классов, которые помечены как @Entity и имеют идентификаторы (@Id). Также заметим, что мы можем настраивать колонки для полей этого класса с помощью аннотации @Column, а также других аннотаций, как если бы класс был бы обыкновенной сущностью, а не значением. В данным случае мы ожидаем, что для поля city будет создана колонка CITY типа VARCHAR(100) (во всяком случае в Postgres).
Теперь создадим класс-сущность следующего содержания:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Address homeAddress; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Мы объявляем поле homeAddress типа Address не снабжая его какой бы то ни было аннотацией. Того факта, что сам класс Address помечен аннотацией @Embeddable достаточно, чтобы Hibernate, создавая таблицу для данных, получаемых из объектов типа Person, создал в ней также колонки CITY, STREET и DETAILS для данных из объекта типа Address, На который ссылается поле homeAddress.
Поскольку сущностью в нашей схеме является только класс Person, то над достаточно создать репозиторий только для него:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Напишем тест и убедимся, что всё работает, как мы и ожидаем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Test void checkEmbedded() { Address homeAddress = new Address("Москва", "Садовая", "д. 10к3, кв. 20"); Person irina = new Person("Ирина", homeAddress); personRepository.save(irina); Person irinaInDb = personRepository.findById(irina.getId()).get(); assertEquals(irina, irinaInDb); } } |
Мы создаём объект типа Address, а затем объект типа Person, который помимо основных полей также ссылается на созданный Address. Затем мы сохраняем данные объекта в БД и следующей строкой извлекаем их по id.
В конце мы убеждаемся, что объект отображается в строку в БД и обратно корректно. Созданный объект и полученный из БД объект эквивалентны (если тест падает, убедитесь, что вы должным образом переопределили методы equals() и hashCode() у обоих классов).
Давайте посмотрим, что Hibernate создал в БД:
Как мы видим все данные полей объектов класса Address (CITY, DETAILS, STREET) хранятся в таблице Person с остальными данными объектов Person.
Также правильно сработала аннотация @Column с параметром length = 100. Размер колонки city действительно 100 символов, в отличие от других строковых колонок. где установлены 255 по умолчанию.