Встраиваемый (@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:
1 2 3 4 5 6 7 8 9 10 11 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Затем создадим встраиваемый класс Address, который будет содержать поле Set<Person> owners. Подразумевается, что адрес — это в конечном итоге объект недвижимости, а у объекты недвижимости есть владельцы. Таким образом встраиваемый класс будет содержать связь один-ко-многим с классом-сущностью.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Embeddable public class Address { private String city; private String location; @OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "ADDRESS_ID") private Set<Person> owners; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Создадим ещё один класс-сущность Company, который будет содержать поле типа Address. Таким образом данные об адресе будут храниться в таблице COMPANY вместе с другими данными о компании:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Entity public class Company { @Id @GeneratedValue private Long id; private String name; private Address address; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Репозитории
Создадим репозиторий для класса Person:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Также создадим репозиторий для класса Company:
1 2 3 |
@Repository public interface CompanyRepository extends JpaRepository<Company, Long> { } |
Поскольку Address является встраиваемым классом, то он не имеет ни соответствующей таблице в БД, ни репозитория в коде.
Проверка кода
Напишем тест, который продемонстрирует работу кода:
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 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Autowired CompanyRepository companyRepository; @Test void oneEmbeddedToManyEntitiesTest() { Set<Person> owners = Set.of( new Person("Vladimir"), new Person("Irina") ); personRepository.saveAll(owners); Address acmeLtdAddress = new Address("Москва", "ул. Ленина, д.1", owners); Company acmeLtd = new Company("Acme", acmeLtdAddress); companyRepository.save(acmeLtd); Company acmeInDb = companyRepository.findById(acmeLtd.getId()).get(); assertTrue(acmeInDb.getAddress().getOwners().containsAll(owners)); } } |
Сначала мы создаём два объекта типа 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:
1 |
FOREIGN KEY (address_id) REFERENCES company(id) |
Таким образом связь один-ко-многим между встраиваемым классом и классом-сущностью в итоге отображается в связь между таблицами. Но так как у встраемго класса своей таблицы нет, то создаётся ссылка на таблицу, в которую данные встраиваемого класса в итоге встраиваются.