Не всегда можно и имеет смысл расширять ту или иную таблицу за счёт добавления в неё новых полей. Зачастую удобней и правильней создать ещё одну таблицу и установить между ними связь типа один-к-одному.
Если сущности слабо связаны друг с другом и не могут опираться на разделяемые значения первичных ключей, то связь между ними можно организовать с помощью внешнего ключа.
Подготовка
Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL
Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:
spring.jpa.hibernate.ddl-auto=update
Код
Создадим класс предметной области Person, соответствующий таблице 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() и т.д. } |
Поскольку связь планируется быть однонаправленной, класс Person ничего не знает о том, что есть другие сущности, которые на него ссылаются.
Создадим класс BiographyExtension данные которого расширяют данные класса Person. Между таблицами двух классов установим отношение один-к-одному:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Entity public class BiographyExtension { @Id @GeneratedValue private Long id; @OneToOne(optional = false, cascade = CascadeType.PERSIST) @JoinColumn(unique = true) private Person person; private String citizenship; private String placeOfBirth; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
С помощью аннотации @OneToOne мы устанавливаем связь типа один-к-одному между BiographyExtension и Person.
Параметр optional равный false указывает Hibernate’у, что во время сохранения объекта BiographyExtension поле Person person обязательно должно ссылаться на существующий объект Person.
Параметр cascade равный CascadeType.PERSIST указывает Hibernate’у, что при сохранении объекта BiographyExtension нужно также сохранить парный объект Person, на который он ссылается.
Таблица BIOGRAPHY_EXTENSION будет присоединена к таблице PERSON через создаваемую автоматически колонку PERSON_ID, на которую будет наложено ограничение внешнего ключа на поле PERSON.ID. С помощью аннотации @JoinColumn с параметром unique = true мы также указываем наложить на эту колонку ограничение UNIQUE, чтобы на каждую строку таблицы PERSON могла ссылаться только одна строка таблицы BIOGRAPHY_EXTENSION. Так мы гарантируем, что наша связь действительно является связью один-к-одному.
Создадим репозиторий для класса Person:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Также создадим репозиторий для класса BiographyExtension:
1 2 3 4 5 |
@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 22 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Autowired BiographyExtensionRepository biographyExtensionRepository; @Test void oneToOneForeignKeyTest() { Person irina = new Person("Irina", 30); BiographyExtension irinasBiography = new BiographyExtension(irina, "Russia", "Angarsk"); biographyExtensionRepository.save(irinasBiography); BiographyExtension irinasBiographyInDb = biographyExtensionRepository.findByPerson(irina).get(0); assertEquals(irinasBiography, irinasBiographyInDb); Person irinaInDb = personRepository.findById(irinasBiography.getPerson().getId()).get(); assertEquals(irina, irinaInDb); } } |
Мы создаём объект irina типа Person, а затем irinasBiography типа BiographyExtension. Ещё в конструкторе мы BiographyExtension мы устанавливаем в поле Person person ссылку на созданный строчкой выше объект irina.
Затем мы сохраняем irinasBiography. Благодаря параметру cascade = CascadeType.PERSIST аннотации @OneToOne вместе с объектом irinasBiography также сохраняется объект irina.
Затем мы извлекаем данные и БД и убеждаемся, что данные в БД эквивалентны данным в коде, то есть сохранение прошло как и ожидалось.
Рассмотрим, как данные отобразились в таблицы:
Для каждого класса-сущности Hibernate создал свой сиквенс.
Посмотрим на схемы таблиц, созданные Hibernate’ом:
В таблице BIOGRAPHY_EXTENSION создана колонка PERSON_ID с внешним ключом на PERSON.ID, что и обеспечивает связь между таблицами. Ограничение UNIQUE на этой колонке гарантирует, что на каждую строку таблицы PERSON, если и есть ссылка из BIOGRAPHY_EXTENSION, то только одна, а значит у нас действительно связь один-к-одному.
Благодаря параметру optional = false аннотации @OneToOne Hibernate сделал PERSON_ID not null полем. Однако это было необязательно. Если та или иная расширяющая сущность может существовать самостоятельно, без привязки к другой сущности, то колонка внешнего ключа может содержать null значения.