Одной из сильных сторон объектного моделирования предметной области заключается в возможности создавать двунаправленные связи между родительскими и дочерними объектами. Так, если пользователю может соответствовать несколько адресов, то не только класс Address будет содержать поле Person, к которому конкретный адрес относится, но и класс Person может содержать список типа Address со всеми относящимися к нему адресами.
В реляционных базах данных невозможно точно воспроизвести такие отношения. Но Hibernate снимает это несоответствие, позволяя разработчику создавать двунаправленные отношения в объектной модели в Java, продолжая сохранять данные в реляционной модели в БД.
Подготовка
Создадим минимальное Spring Boot приложение с поддержкой JPA/Hibernate.
Код
Создадим класс предметной области следующего содержания:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Entity public class Address { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "PERSON_ID", nullable = false) private Person person; @Column(nullable = false) private String city; @Column(nullable = false) private String street; @Column(nullable = false) private String house; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Аннотация @ManyToOne над полем Person person указывает Hibernate’у создать связь вида многие-к-одному между сущностями Address и Person. С точки зрения БД, в таблице ADDRESS будет создано поле с внешним ключом под названием PERSON_ID (так как мы ссылаемся на таблицу PERSON, в которой первичный ключ — колонка ID). Аннотация @ManyToOne — единственное, что требуется для установления связи многие-к-одному.
Кроме того, мы передаём в аннотацию @ManyToOne параметр fetch = FetchType.LAZY, который предписывает Hibernate’у не запрашивать в БД данные для заполнения этого поля, пока не будет вызван геттер getPerson(). По умолчанию значение этого параметра FetchType.EAGER (т.е. данные запрашиваются сразу). Отметим, что для правильной работы этого функционала вызов геттера должен происходить в рамках Hibernate сессии. В Spring приложении для этого достаточно пометить метод, в котором вызывается геттер (или весь класс) аннотацией @Transactional (сработает как @jakarta.transaction.Transactional, так и @org.springframework.transaction.annotation.Transactional).
Использование аннотации @JoinColumn носит необязательный характер. Но в ней можно переопределить название колонки внешнего ключа (мы оставили его таким же, как если бы Hibernate создал его автоматически), а также добавить этой колонке некоторые ограничения. В нашем примере мы делаем её NOT NULL колонкой, из-за чего связь между адресом и персоной становится обязательной (но могла бы таковой и не быть).
Создадим класс Person, на который мы сослались выше, и который в свою очередь будет содержать список объектов типа Address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Integer age; @OneToMany(mappedBy = "person", fetch = FetchType.LAZY) private List<Address> addresses; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Аннотация @OneToMany завершает создание двусторонней связи между сущностями Person и Address. В нашем примере в неё передаётся один обязательный параметр — mappedBy с именем поля в классе Address, которое там ссылается на родительскую сущность Person.
Кроме того, мы передали необязательный параметр fetch = FetchType.LAZY, который указывает Hibernate’у не запрашивать в БД дочерние строки Address, пока пользователь явно не вызовет геттер getAddresses(). Такое поведение является поведением по умолчанию. Мы явно его прописали в демонстрационных целях. Доступ к отложенной (lazy) загрузке данных через геттер также требует открытой Hibernate сессии (см. выше).
Если же задать параметр fetch равным FetchType.EAGER, то строки Address будут автоматически загружаться вместе с родительским объектом Person. Обычно это не совсем то, что нужно в контексте оперирования самостоятельными сущностями. Но может быть востребовано при определённых обстоятельствах.
Создадим репозиторий для класса Address:
1 2 3 4 |
@Repository public interface AddressRepository extends JpaRepository<Address, Long> { List<Address> findByPerson(Person person); } |
Также создадим репозиторий для класса Person:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Напишем тестовый метод, который продемонстрирует работу настроек:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@SpringBootTest @Transactional class TestApplicationTests { @Autowired PersonRepository personRepository; @Autowired AddressRepository addressRepository; @Test void bidirectionalTest() { Person irina = new Person("irina", 28); List<Address> irinasAddresses = List.of( new Address(irina, "Москва", "Садовая", "12"), new Address(irina, "Воскресенск", "СНТ Строитель", "424") ); irina.setAddresses(irinasAddresses); personRepository.save(irina); addressRepository.saveAll(irinasAddresses); List<Address> addressesInDb = addressRepository.findByPerson(irina); Person irinaInDb = personRepository.findById(irina.getId()).get(); assertTrue(irinaInDb.getAddresses().containsAll(addressesInDb)); assertEquals(addressesInDb.get(0).getPerson(), irinaInDb); } } |
Мы создаём объект irina типа Person и список из двух объектов типа Address. Мы устанавливаем ссылки объектов друг на друга. У каждого объекта Address поле person заполняется ссылкой на объект irina в конструкторе. Затем список Address устанавливается в поле addresses объекта irina через сеттер: irina.setAddresses(irinasAddresses)
. После этого мы сохраняем объекты в БД.
Далее мы извлекаем данные из БД и видим, что Hibernate восстановил двустороннюю связь между сохранёнными сущностями. Утверждение assertTrue(irinaInDb.getAddresses().containsAll(addressesInDb))
показывает, что объект irinaInDb с извлечёнными из БД данными возвращает методом getAddresses() точно такой же список адресов, как мы сами извлекаем из БД поиском по объекту irina. Объекты списка addressesInDb в свою очередь содержат ссылки на объект типа Person полностью аналогичный извлечённому руками: assertEquals(addressesInDb.get(0).getPerson(), irinaInDb)
.
Что и демонстрирует двустороннюю связь между сущностями.