Давайте представим себе, что вы разрабатываете самый обычный 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/
Добавить комментарий