Создание однонаправленной связи один-к-одному со связыванием через третью таблицу

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

Если сущности слабо связаны друг с другом и не могут опираться на разделяемые значения первичных ключей, то связь между ними можно организовать либо с помощью внешних ключей, либо с помощью промежуточной таблицы. Если мы не можем допустить наличия NULL значений в таблице, то вариант с внешним ключом может не подойти и необходимо использовать третью таблицу для связи.

Подготовка

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

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

spring.jpa.hibernate.ddl-auto=update

Код

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

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

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

Чтобы установить связь один-к-одному мы помечаем поле Person person аннотацией @OneToOne.

В аннотации @JoinTable мы описываем промежуточную таблицу, с помощью которой мы хотим создать связь между таблицами PERSON и BIOGRAPHY_EXTENSION.

В параметре name мы задаём имя соединяющей таблицы.

В параметре joinColumns мы указываем название колонки соединяющей таблицы, которая будет ссылаться на текущую таблицу (BIOGRAPHY_EXTENSION).

В параметре inverseJoinColumns мы указываем название колонки соединяющей таблицы, которая будет ссылаться на парную таблицу (PERSON).

С помощью параметров nullable и unique мы накладываем на описываемые колонки соответствующие ограничения. В данном случае мы задаём эти ограничения только для колонки, ссылающейся на парную таблицу.

Репозитории

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

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

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

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

Сперва запустим тестовый метод oneToOneJoinTableTest и посмотрим состояние БД:

Добавим ещё один тестовый метод следующего содержания:

Запустим второй тестовый метод и посмотрим, как изменятся данные третей (связующей) таблицы:

Использование связующей таблицы дало нам определённое преимущество. Данные сущностей PERSON и BIOGRAPY_EXTENSION могут существовать и храниться в БД независимо друг от друга. При этом ни в одной из таблиц у нас не будет колонок, содержащих null’ы. Но при необходимости мы можем в любой момент создать связь между двумя строками.

Таким образом третья таблица даёт определённую гибкость в обеспечении жизненного цикла сущностей. Платой за гибкость являются накладные расходы на использование join’ов в запросах.

Рассмотрим схему БД, которую создал Hibernate:

Из схемы видно, что таблицы PERSON и BIOGRAPY_EXTENSION устроены таким образом будто никакой связи между ними нет. Хотя в описании класса BiograpyExtension есть и поле Person, и аннотация @OneToOne, на самой таблице это никак не отражается.

Всё бремя обеспечения связи лежит на промежуточной таблице PERSON_BIOGRAPHY. Обе колонки таблицы являются внешними ключами на связываемые таблицы. Внешний ключ на таблицу BIOGRAPY_EXTENSION также считается первичным ключом данной. А на внешний ключ на таблицу PERSON мы наложили ограничения NOT NULL и UNIQUE с помощью параметров nullable = false и unique = true, чтобы гарантировать, что любая запись в данной таблице обязательно является связью между чем-то и чем-то. И, что любая связь всегда уникальна, так как речь идёт о связи типа один-к-одному.