Не всегда можно и имеет смысл расширять ту или иную таблицу за счёт добавления в неё новых полей. Зачастую удобней и правильней создать ещё одну таблицу и установить между ними связь типа один-к-одному.
Если сущности слабо связаны друг с другом и не могут опираться на разделяемые значения первичных ключей, то связь между ними можно организовать либо с помощью внешних ключей, либо с помощью промежуточной таблицы. Если мы не можем допустить наличия NULL значений в таблице, то вариант с внешним ключом может не подойти и необходимо использовать третью таблицу для связи.
Подготовка
Создадим базовое веб-приложения на связке 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; private Integer age; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Создадим класс BiographyExtension, данные которого дополняют данные Person, и установим между соответствующими таблицами связь по типу один-к-одному через промежуточную третью таблицу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Entity public class BiographyExtension { @Id @GeneratedValue private Long id; @OneToOne @JoinTable( name = "PERSON_BIOGRAPHY", joinColumns = @JoinColumn(name = "BIOGRAPHY_ID"), inverseJoinColumns = @JoinColumn(name = "PERSON_ID", nullable = false, unique = true) ) private Person person; private String citizenship; private String placeOfBirth; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Чтобы установить связь один-к-одному мы помечаем поле Person person аннотацией @OneToOne.
В аннотации @JoinTable мы описываем промежуточную таблицу, с помощью которой мы хотим создать связь между таблицами PERSON и BIOGRAPHY_EXTENSION.
В параметре name мы задаём имя соединяющей таблицы.
В параметре joinColumns мы указываем название колонки соединяющей таблицы, которая будет ссылаться на текущую таблицу (BIOGRAPHY_EXTENSION).
В параметре inverseJoinColumns мы указываем название колонки соединяющей таблицы, которая будет ссылаться на парную таблицу (PERSON).
С помощью параметров nullable и unique мы накладываем на описываемые колонки соответствующие ограничения. В данном случае мы задаём эти ограничения только для колонки, ссылающейся на парную таблицу.
Репозитории
Создадим репозиторий для класса Person:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Также создадим репозиторий для класса BiographyExtension:
1 2 3 |
@Repository public interface BiographyExtensionRepository extends JpaRepository<BiographyExtension, Long> { } |
Проверка кода
Напишем тест, который продемонстрирует работу кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Autowired BiographyExtensionRepository biographyExtensionRepository; @Test void oneToOneJoinTableTest() { Person irina = new Person("Irina", 30); personRepository.save(irina); BiographyExtension irinasBiography = new BiographyExtension("Russia", "Angarsk"); biographyExtensionRepository.save(irinasBiography); } } |
Сперва запустим тестовый метод oneToOneJoinTableTest и посмотрим состояние БД:
Добавим ещё один тестовый метод следующего содержания:
1 2 3 4 5 6 7 8 9 |
@Test void oneToOneJoinTableStateTest() { Person person = personRepository.findAll().get(0); BiographyExtension biography = biographyExtensionRepository.findAll().get(0); biography.setPerson(person); biographyExtensionRepository.save(biography); } |
Запустим второй тестовый метод и посмотрим, как изменятся данные третей (связующей) таблицы:
Использование связующей таблицы дало нам определённое преимущество. Данные сущностей PERSON и BIOGRAPY_EXTENSION могут существовать и храниться в БД независимо друг от друга. При этом ни в одной из таблиц у нас не будет колонок, содержащих null’ы. Но при необходимости мы можем в любой момент создать связь между двумя строками.
Таким образом третья таблица даёт определённую гибкость в обеспечении жизненного цикла сущностей. Платой за гибкость являются накладные расходы на использование join’ов в запросах.
Рассмотрим схему БД, которую создал Hibernate:
Из схемы видно, что таблицы PERSON и BIOGRAPY_EXTENSION устроены таким образом будто никакой связи между ними нет. Хотя в описании класса BiograpyExtension есть и поле Person, и аннотация @OneToOne, на самой таблице это никак не отражается.
Всё бремя обеспечения связи лежит на промежуточной таблице PERSON_BIOGRAPHY. Обе колонки таблицы являются внешними ключами на связываемые таблицы. Внешний ключ на таблицу BIOGRAPY_EXTENSION также считается первичным ключом данной. А на внешний ключ на таблицу PERSON мы наложили ограничения NOT NULL и UNIQUE с помощью параметров nullable = false и unique = true, чтобы гарантировать, что любая запись в данной таблице обязательно является связью между чем-то и чем-то. И, что любая связь всегда уникальна, так как речь идёт о связи типа один-к-одному.