Создание двунаправленной связи один-к-одному с общим значением поля ID обоих сущностей, устанавливаемым автоматически

Не всегда можно и имеет смысл расширять ту или иную таблицу за счёт добавления в неё новых полей. Зачастую удобней и правильней создать ещё одну таблицу и установить между ними связь типа один-к-одному. В БД связь может поддерживаться через общие значения первичных ключей. Аналогично необходимо создать подобную связь между отображаемыми в эти таблицы сущностями.

Такую связь можно сделать двунаправленной. И расширяющая, и расширяемая сущности в таком случае ссылаются друг на друга. Кроме того, если связь двунапрвленная и JPA провайдером в проекте является Hibernate, то можно значительно облегчить создание общего ID и сохранение связанных сущностей в БД.

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

Создадим класс предметной области Person следующего содержания:

Поскольку связь между сущностями двунаправленная, то класс Person также содержит поле типа BiographyExtension со ссылкой на объект расширяющего класса.

Поле BiographyExtension biographyExtension помечается аннотацией @OneToOne. В параметре mappedBy мы указываем имя поля в классе BiographyExtension, которое в свою очередь будет ссылаться на класс Person. Всегда, когда речь идёт о двунаправленной связи, с одной из сторон появляется параметр mappedBy внутри той или иной аннотации.

С помощью параметра cascade со значением CascadeType.PERSIST мы указываем Hibernate’у при сохранении данных объекта Person автоматически сохранять данные парного объекта BiographyExtension. Что довольно удобно, так как иначе это придётся делать вручную.

Создадим класс BiographyExtension, второй класс этой двунаправленной связи:

Класс BiographyExtension содержит поле id, размеченное особым образом. Hibernate позволяет написать и использовать специальный генератор id, который будет брать значение id из соответствующего поля парного объекта Person.

Аннотация @GeneratedValue с параметром generator равным «personsForeignGenerator» указывает Hibernate’у использовать кастомный генератор с названием personsForeignGenerator.

С помощью аннотации @org.hibernate.annotations.GenericGenerator мы собственно и создаём этот кастомный генератор. Параметр name отвечает за имя генератора. Параметр strategy со значением «foreign» указывает брать id внешней сущности. Значение параметра @org.hibernate.annotations.Parameter(name = «property», value = «person») описывает, в каком поле есть ссылка на сущность, у которой нужно брать id для постановки.

В классе BiographyExtension есть поле Person person, у класса Person есть поле id. И при заполнении поля id объекта класса BiographyExtension значение будет браться из Person.id.

Аннотация @OneToOne создаёт связь типа один-к-одному между двумя сущностями. Параметр optional = false указывает, что объект класса BiographyExtension не может сохраняться, не имея ссылки на парный объект Person.

Аннотация @PrimaryKeyJoinColumn указывает Hibernate’у, что поле ID таблицы BIOGRAPHY_EXTENSION будет также являться внешним ключом на поле ID таблицы PERSON.

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

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

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

В данном примере мы создаём объект irina типа Person и irinasBiography типа BiographyExtension. При этом мы вручную устанавливаем этим объектам ссылки друг на друга (в одном случае в конструкторы, в другом — с помощью сеттера).

Затем мы сохраняем только объект Person irina. Благодаря параметру cascade = CascadeType.PERSIST аннотации @OneToOne Hibernate также сохранит данные объекта irinasBiography.

Затем мы извлекаем данные из БД и доказываем, что сущности в коде и сущности, извлечённые из БД, эквивалентны, что свидетельствует о том, что персистенция выполняется должным образом.

Посмотрим состояние БД:

Hibernate создал сиквенс только для таблицы PERSON, так как ID таблицы BIOGRAPHY_EXTENSION должен дублировать ID парной записи PERSON благодаря кастомному генератору.

Рассмотрим схему созданных таблиц:

Как мы и ожидали благодаря аннотации @PrimaryKeyJoinColumn первичный ключ таблицы BIOGRAPHY_EXTENSION также является внешним ключом на поле ID таблицы PERSON, что дополнительно гарантирует нам согласованность данных.