Не всегда можно и имеет смысл расширять ту или иную таблицу за счёт добавления в неё новых полей. Зачастую удобней и правильней создать ещё одну таблицу и установить между ними связь типа один-к-одному. В БД связь может поддерживаться через общие значения первичных ключей. Аналогично необходимо создать подобную связь между отображаемыми в эти таблицы сущностями.
Проще всего сделать такую связь однонаправленной. Расширяемая сущность в таком случае не содержит поля, ссылающегося на расширяющую. Она может иметь несколько расширяющих сущностей/таблиц и «не знать» об этом. Расширяющая сущность связывается с расширяемой с помощью аннотации @OneToOne.
Подготовка
Создадим базовое веб-приложения на связке 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 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Integer age; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Создадим вторую сохраняемую сущность, таблицу которой планируется соединить с таблицей предыдущей сущности связью один-к-одному:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Entity public class BiographyExtension { @Id private Long id; @OneToOne(optional = false) @PrimaryKeyJoinColumn private Person person; private String citizenship; private String placeOfBirth; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Поскольку мы организовываем одностороннюю связь между сущностями, то в Person у нас нет никаких указаний на расширение, в то же время в BiographyExtension мы создаём поле Person и размечаем его соответствующими аннотациями:
Аннотация @OneToOne с параметром optional = false указывает Hibernate’у создать связь типа один-к-одному от BiographyExtension к Person. Параметр optional определяет, может ли объект BiographyExtension сохранятся в БД не будучи привязанным к тому или иному объекту Person. Поскольку мы связываем таблицы через общий первичный ключ, то непривязанные объекты BiographyExtension мы сохранять не можем, поэтому при таком подходе мы всегда устанавливаем параметр optional равный false.
Аннотация @PrimaryKeyJoinColumn указывает Hibernate’у, что поле ID таблицы BIOGRAPHY_EXTENSION будет также являться внешним ключом на поле ID таблицы PERSON. Собственно это ограничение внешнего ключа и свяжет две таблицы, гарантируя нам, что у любой id таблицы BIOGRAPHY_EXTENSION равен какому-нибудь id таблицы PERSON.
Создадим репозиторий для сущности Person:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
А также репозиторий для сущности BiographyExtension:
1 2 3 4 |
@Repository public interface BiographyExtensionRepository extends JpaRepository<BiographyExtension, Long> { List<BiographyExtension> findByPerson(Person person); } |
Напишем тест, который продемонстрирует работу кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Autowired BiographyExtensionRepository biographyExtensionRepository; @Test void oneToOneSharedPkTest() throws Exception { Person irina = new Person("Irina", 30); personRepository.save(irina); BiographyExtension irinasBiography = new BiographyExtension(irina.getId(), "Russia", "Angarsk"); biographyExtensionRepository.save(irinasBiography); BiographyExtension irinasBiographyInDb = biographyExtensionRepository.findByPerson(irina).get(0); assertEquals(irina, irinasBiographyInDb.getPerson()); } } |
Сперва мы создаём объект irina типа Person, в котором в конструкторе заполняем все поля кроме id, который будет создан автоматически при сохранении данных в БД. Затем мы собственно сохраняем объект irina.
Затем мы создаём объект типа BiographyExtension. Здесь в конструкторе мы указываем значения всех полей, включая id. При данном подходе это забота программиста сделать так, чтобы каждый объект BiographyExtension содержал такое значение поля id, которое бы соответствовало какой-нибудь строке таблицы PERSON. Затем мы сохраняем объект irinasBiography в БД.
Затем мы извлекаем данные из таблицы BIOGRAPHY_EXTENSION. При описываемом подходе автоматически будут запрошены данные таблицы PERSON, от которой зависит объект BiographyExtension. Поэтому мы можем убедиться, что строки таблиц и соответственно объекты java связаны друг с другом должным образом. Конечно, учитывая, что мы создавали связь в ручную, особых сомнений у нас в этом и не было.
Посмотрим на состояние БД:
В соответствии с нашими настройками Hibernate создал только один сиквенс — PERSON_SEQ. Для таблицы PERSON первичный ключ будет генерироваться в момент сохранения. А вот для таблицы BIOGRAPHY_EXTENSION задавать значение первичного ключа необходимо руками.
Посмотрим схему созданных Hibernate’ом таблиц:
Из схемы видно, что первичный ключ расширяющей таблицы BIOGRAPHY_EXTENSION в то же врем является внешним ключом на PERSON.ID. Таким образом обе таблицы разделяют значения первичного ключа, сгенерированного для таблицы PERSON.
И если в БД за согласованность отвечает ограничение FOREIGN KEY, то в коде мы обеспечиваем эту согласованность, вручную заполняя поле id сущности BiographyExtension.