Как получать сущности со связями в Spring Rest контроллере

от автора

Давайте представим себе, что вы разрабатываете самый обычный REST сервис на стеке Spring, например, с использованием таких зависимостей (build.gradle):

... dependencies { ... implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' ... } 

Все идет как обычно. Вы пишете репозиторий для сущности User:

public interface UserRepository extends CrudRepository<User, UUID> { }  

Затем пишете самый обычный сервис для работы с пользователем, в котором есть метод для получения пользователя по его Id:

public class UserService {     ...     public User getUser(UUID id) {         return userRepository.findById(id)                 .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));     }     ... } 

И, наконец, используете этот метод в вашем REST контроллере, чтобы получать данные пользователя по REST API извне:

@RestController @RequestMapping("/users") public class UserController {     ...     @GetMapping(path = "/{id}")     public User getUser(@PathVariable UUID id) {         return userService.getUser(id);     }     ... } 

Я намеренно не останавливаюсь на настройках подключения Spring к базе данных в application.properties или каким-то иным способом, а также прочих деталях настройки и создания сервиса, предполагая, что вы это уже умеете, база данных у вас есть и доступна, пользователи в ней созданы, класс сущности User также создан в приложении.

Далее наступает момент тестирования приложения. Воспользуемся для этого Postman. Прежде всего, запускаем наше приложение (предположим, мы работаем в Intellij Idea) и видим, что оно успешно стартовало:

Теперь переходим в Postman и пытаемся выполнить GET запрос к методу REST API для получения пользователя, о котором мы наверняка знаем, что он есть в БД, по его Id:

Какой ужас! Мы видим, что вообще не можем получить никакого ответа на наш запрос, даже 500 Interal error! В чем же дело? Ведь это, казалось бы, самый обычный REST сервис. Далее, как уже более-менее опытный junior разработчик, вы снова заходите в консоль Intellij Idea и видите такую ошибку:

Итак, давайте разберемся, в чем дело. По логу мы видим, что у нас есть какие-то сущности Contact и Profile (причем тут они, ведь мы работали с сущностью User?), и Spring не способен сформировать ответ в формате JSON для нашего REST запроса, потому что попадает в бесконечную рекурсию. Почему? Вот теперь начинается самое интересное.

Давайте теперь подробно посмотрим, что такое происходит с нашим пользователем. Прежде всего, заглянем в саму сущность User:

... @Entity @Getter @Setter @ToString @RequiredArgsConstructor public class User {     @Id     @GeneratedValue     private UUID id;      @OneToOne(cascade = CascadeType.ALL)     @JoinColumn(name = "profile_id", referencedColumnName = "id")     private Profile profile;     ... }  

Как интересно! Оказывается, данные пользователя хранятся у нас не только в сущности User, но и в привязанной к нему сущности Profile. Вы сделали такую связь один-к-одному, но забыли об этом.

Заглянем в сущность Profile:

... @Entity @Getter @Setter @ToString @RequiredArgsConstructor public class Profile {     @Id     @GeneratedValue     private UUID id;      private String name;      private String surname;      @Column(name = "second_name")     private String secondName;      @Column(name = "birth_date")     private Instant birthDate;      @Column(name = "avatar_link")     private String avatarLink;      private String information;      @Column(name = "city_id")     private Integer cityId;      @OneToOne(cascade = CascadeType.ALL)     @JoinColumn(name = "contact_id", referencedColumnName = "id")      private Contact contact;      @Column(name = "gender_id")     private UUID gender_id;      @OneToOne(mappedBy = "profile")     User user;  } ... 

Упс! Помимо «обратного конца» связи один-к-одному профиля с пользователем

    @OneToOne(mappedBy = "profile")     User user; 

мы видим, что и сам профиль связан по типу один-к-одному еще с одной сущностью — Contact. То есть вы выделили контакты пользователя также в отдельную сущность. Можем взглянуть и на нее:

@Entity @Getter @Setter @RequiredArgsConstructor @ToString public class Contact {     @Id     @GeneratedValue     private UUID id;      private String email;      private String phone;      @OneToOne(mappedBy = "contact")     Profile profile; }  

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

Итак, у нас есть три взаимосвязанных сущности — пользователь, профиль и контакт. Не будем на этом подробно останавливаться, но очевидно, что вы и в БД наложили на них соответствующие constraint, примерно как-то так:

alter table users_scheme.user add constraint user_profile_fk foreign key (profile_id) references users_scheme.profile(ID) on delete cascade on update cascade; alter table users_scheme.profile add constraint profile_contact_fk foreign key (contact_id) references users_scheme.contact(ID) on delete cascade on update cascade; 

Именно поэтому вы добавили реализацию этих ограничений в ваши классы сущностей, чтобы Hibernate мог нормально работать с ними. Но он не работает.

О причине, вероятно, вы уже догадались, если вспомнили, что взаимосвязь один-к-одному у нас двухсторонняя. Поэтому при попытке сериализовать экземпляр класса пользователя fasterxml jackson попадает в цикл и не может этого сделать. Вывод — нужно этот цикл каким-то образом обработать, сделав его не бесконечным, или прервать.

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

Существуют и другие способы сделать то же самое, но они работают иначе и могут не сработать вообще или не выдать такую же полную информацию. Поэтому я остановлюсь на том, чтобы показать вам только этот. Возможно, другие способы я еще рассмотрю в других статьях.

Итак, что нужно сделать. Маркируем в каждой из сущностей следующие поля вот такой аннотацией:

//пользователь @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "profile_id", referencedColumnName = "id") @JsonIdentityInfo(         generator = ObjectIdGenerators.PropertyGenerator.class,         property = "id") private Profile profile;  //профиль @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "contact_id", referencedColumnName = "id") @JsonIdentityInfo(         generator = ObjectIdGenerators.PropertyGenerator.class,         property = "id") private Contact contact;  //контакт @OneToOne(mappedBy = "contact") @JsonIdentityInfo(         generator = ObjectIdGenerators.PropertyGenerator.class,         property = "id")     Profile profile; 

После чего перезапускаем приложение и повторяем запрос в Postman:

Вот теперь все в полном порядке, мы получили нормальный JSON ответ с данными о пользователе из всех трех связанных сущностей. В консоли IDE также ошибок больше нет.


Скоро в OTUS состоится открытое занятие «Аспекты в Java и в Spring», на котором рассмотрим аспекты — что это и зачем нужно; как создавать аспекты в Java, используя разные технологии, и как они используются в Spring. Регистрация для всех желающих — по ссылке.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/679722/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *