Как стать автором
Обновить
1116.59
OTUS
Цифровые навыки от ведущих экспертов

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

Время на прочтение4 мин
Количество просмотров10K

Давайте представим себе, что вы разрабатываете самый обычный 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. Регистрация для всех желающих — по ссылке.

Теги:
Хабы:
Всего голосов 18: ↑10 и ↓8+3
Комментарии18

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS