Связь @OneToMany между @Embeddable и @Entity классами в Hibernate

Встраиваемый (@Embeddable) класс может содержать поле со списком объектов класса-сущности (@Entity). При этом у данных встраиваемого класса нет своей таблицы и они, как и положено, хранятся в таблице какой-то ещё сущности. Такая организация приводит к возникновению связи один-ко-многим между таблицами классов сущностей, которые в коде напрямую друг с другом не связаны.

Например, если у нас класс-сущность Company, содержащий поле встраиваемого класса Address при этом в классе Address есть поле List<Person> owners, где Person это в свою очередь класс-сущность, то между таблицами COMPANY и PERSON создаётся связь один-ко-многим, хотя между классами Company и Person прямой связи нет.

Подготовка

Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL

Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:

spring.jpa.hibernate.ddl-auto=update

Код

Классы предметной области

Создадим класс предметной области Person:

Затем создадим встраиваемый класс Address, который будет содержать поле Set<Person> owners. Подразумевается, что адрес — это в конечном итоге объект недвижимости, а у объекты недвижимости есть владельцы. Таким образом встраиваемый класс будет содержать связь один-ко-многим с классом-сущностью.

Создадим ещё один класс-сущность Company, который будет содержать поле типа Address. Таким образом данные об адресе будут храниться в таблице COMPANY вместе с другими данными о компании:

Репозитории

Создадим репозиторий для класса Person:

Также создадим репозиторий для класса Company:

Поскольку Address является встраиваемым классом, то он не имеет ни соответствующей таблице в БД, ни репозитория в коде.

Проверка кода

Напишем тест, который продемонстрирует работу кода:

Сначала мы создаём два объекта типа Person (собственники) и, так как это полноценные сущности, сохраняем их данные в БД непосредственно с помощью соответствующего репозитория.

Затем мы создаём объект Address acmeLtdAddress, который не является сущностью и не может быть сохранён самостоятельно. В конструктор объекта acmeLtdAddress помимо строки с собственно адресом мы также передаём множество (Set) ранее созданных собственников.

В конце мы создаём объект Company acmeLtd, в конструктор которого помимо названия мы также передаём объект адреса и сохраняем данные в БД.

Затем мы по id находим данные о сохранённой компании в БД и извлекаем их в объект Company acmeInDb. После чего доказываем, что извлечённая компания ссылается на адрес, который в свою очередь ссылается на множество Person, которое содержит все ранее созданные объекты Person.

Таким образом, хотя мы и не устанавливали прямой связи между таблицами COMPANY и PERSON, но у нас есть связь типа один-ко-многим между встраиваемым классом и классом-сущностью. Что в итоге приводит к созданию прямых связей между таблицами и опосредованных связей между классами.

Рассмотрим состояние таблиц в БД после выполнения кода:

Как мы и ожидали классам-сущностям Company и Person созданы отдельные таблицы с собственными сиквенсами. Для встраиваемого класса Address отдельной таблицы нет, но данные его полей сохраняются в таблицу COMPANY. Поскольку между классами Address и Person в коде установлена связь один-ко-многим, то таблица PERSON помимо прочего также содержит поле address_id, которое ссылается на id таблицы COMPANY.

Рассмотрим схему таблиц, созданную Hibernate’ом:

В схеме это хорошо видно по описанию таблицы PERSON:

Таким образом связь один-ко-многим между встраиваемым классом и классом-сущностью в итоге отображается в связь между таблицами. Но так как у встраемго класса своей таблицы нет, то создаётся ссылка на таблицу, в которую данные встраиваемого класса в итоге встраиваются.