JavaRush /Курсы /Модуль 5. Spring /Лекция 294: GraphQL в асинхронных системах

Лекция 294: GraphQL в асинхронных системах

Модуль 5. Spring
30 уровень , 3 лекция
Открыта

В реальной жизни данные не всегда живут в одной базе данных или одном сервисе. Иногда нужно отправить запросы к нескольким микросервисам или базам данных одновременно, объединить результаты и ответить клиенту. Асинхронные вызовы в GraphQL — это механизм, позволяющий обрабатывать такие задачи эффективно, не блокируя основной поток выполнения.

Давайте разберем это на простом примере: представьте, что вы разрабатываете приложение для бронирования авиабилетов. Клиент запрашивает информацию о рейсе, а также отзывы и рейтинг авиакомпании. Отзывы и рейтинг находятся в одной базе данных, а данные о рейсе поступают из внешнего API. Вызов всех этих сервисов последовательно означает замедление выполнения запроса. Асинхронная обработка позволяет запустить все запросы параллельно.


Как GraphQL поддерживает асинхронность?

Spring GraphQL поддерживает использование асинхронных вызовов благодаря CompletableFuture из Java. Это стандартный инструмент для работы с Future-объектами, который позволяет организовать цепочки асинхронных задач и прозрачно обрабатывать их результаты.

Асинхронные операции в GraphQL организуются на уровне Data Fetchers (обработчиков данных). Вместо того чтобы возвращать результат сразу, Data Fetcher возвращает объект CompletableFuture, который заполняется данными после завершения асинхронного выполнения.


Асинхронные Data Fetchers в Spring GraphQL

Пример базового асинхронного Data Fetcher

Рассмотрим, как реализовать простейший асинхронный обработчик в Spring GraphQL. В вашем приложении мы добавим обработчик, который получает данные из внешнего API.


@Component
public class FlightDataFetcher implements DataFetcher<CompletableFuture<Flight>> {

    private final FlightService flightService;

    public FlightDataFetcher(FlightService flightService) {
        this.flightService = flightService;
    }

    @Override
    public CompletableFuture<Flight> get(DataFetchingEnvironment environment) {
        String flightId = environment.getArgument("id");
        // Асинхронный вызов сервиса для получения данных о рейсе
        return CompletableFuture.supplyAsync(() -> flightService.getFlightById(flightId));
    }
}

Здесь мы используем CompletableFuture.supplyAsync для асинхронного получения данных. Это стандартный метод Java для выполнения задачи в отдельном потоке.

Интеграция асинхронного Data Fetcher в схему GraphQL

Теперь создадим GraphQL-схему, которая вызывает асинхронный Fetcher:


type Query {
    flight(id: ID!): Flight
}

type Flight {
    id: ID!
    airline: String!
    departureTime: String!
    arrivalTime: String!
}

И регистрируем наш Fetcher в GraphQLRuntimeWiring:


@Configuration
public class GraphQLConfig {

    private final FlightDataFetcher flightDataFetcher;

    public GraphQLConfig(FlightDataFetcher flightDataFetcher) {
        this.flightDataFetcher = flightDataFetcher;
    }

    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer() {
        return wiringBuilder -> wiringBuilder
                .type("Query", typeWiring -> typeWiring
                        .dataFetcher("flight", flightDataFetcher));
    }
}

Асинхронные вызовы в цепочке

Асинхронность становится ещё более полезной, когда нужно объединить несколько запросов. Например, для запроса информации о рейсе и авиакомпании, к которой он относится.


@Component
public class AirlineDataFetcher implements DataFetcher<CompletableFuture<Airline>> {

    private final AirlineService airlineService;

    public AirlineDataFetcher(AirlineService airlineService) {
        this.airlineService = airlineService;
    }

    @Override
    public CompletableFuture<Airline> get(DataFetchingEnvironment environment) {
        String airlineId = environment.getArgument("airlineId");
        return CompletableFuture.supplyAsync(() -> airlineService.getAirlineById(airlineId));
    }
}

Теперь используем асинхронное объединение данных:


@Component
public class FlightDetailsFetcher implements DataFetcher<CompletableFuture<FlightDetails>> {

    private final FlightService flightService;
    private final AirlineService airlineService;

    public FlightDetailsFetcher(FlightService flightService, AirlineService airlineService) {
        this.flightService = flightService;
        this.airlineService = airlineService;
    }

    @Override
    public CompletableFuture<FlightDetails> get(DataFetchingEnvironment environment) {
        String flightId = environment.getArgument("id");

        CompletableFuture<Flight> flightFuture = CompletableFuture.supplyAsync(() -> flightService.getFlightById(flightId));
        CompletableFuture<Airline> airlineFuture = flightFuture.thenCompose(flight ->
                CompletableFuture.supplyAsync(() -> airlineService.getAirlineById(flight.getAirlineId())));

        // Объединяем данные о рейсе и авиакомпании в один объект
        return flightFuture.thenCombine(airlineFuture, (flight, airline) -> {
            return new FlightDetails(flight, airline);
        });
    }
}

В этом примере используется метод thenCombine, который объединяет два CompletableFuture и создает итоговый объект FlightDetails.


Подходы к управлению асинхронностью

  1. Ошибки и таймауты. В асинхронных системах нужно предусматривать обработку ошибок, особенно если запрос идет к внешним API. Лучший способ — обернуть вызов в try-catch блок и возвращать заранее предопределенные значения в случае ошибок.
  2. Оптимизация производительности. Используйте пулы потоков (ThreadPoolExecutor), чтобы ограничить количество одновременно работающих потоков. Это защитит вашу систему от перегрузки.
  3. Сложные зависимости. Если один запрос зависит от другого, старайтесь минимизировать последовательность вызовов. Например, объединяйте данные уже на уровне базы данных, если это возможно.

Практика: Создание сложных асинхронных запросов

Предположим, у нас есть следующие сущности:

  • Flight (информация о рейсе).
  • Airline (информация об авиакомпании).
  • Reviews (отзывы на авиакомпанию).

Задача: создать GraphQL-запрос, который возвращает рейс, авиакомпанию и отзывы за один вызов.

В GraphQL-схеме:


type Query {
    flightDetails(id: ID!): FlightDetails
}

type FlightDetails {
    flight: Flight
    airline: Airline
    reviews: [Review]
}

Реализация Fetcher:


@Component
public class FlightDetailsWithReviewsFetcher implements DataFetcher<CompletableFuture<FlightDetailsWithReviews>> {

    private final FlightService flightService;
    private final AirlineService airlineService;
    private final ReviewService reviewService;

    public FlightDetailsWithReviewsFetcher(FlightService flightService, AirlineService airlineService, ReviewService reviewService) {
        this.flightService = flightService;
        this.airlineService = airlineService;
        this.reviewService = reviewService;
    }

    @Override
    public CompletableFuture<FlightDetailsWithReviews> get(DataFetchingEnvironment environment) {
        String flightId = environment.getArgument("id");

        CompletableFuture<Flight> flightFuture = CompletableFuture.supplyAsync(() -> flightService.getFlightById(flightId));
        CompletableFuture<Airline> airlineFuture = flightFuture.thenCompose(flight ->
                CompletableFuture.supplyAsync(() -> airlineService.getAirlineById(flight.getAirlineId())));
        CompletableFuture<List<Review>> reviewsFuture = airlineFuture.thenCompose(airline ->
                CompletableFuture.supplyAsync(() -> reviewService.getReviewsForAirline(airline.getId())));

        return flightFuture.thenCombine(airlineFuture, (flight, airline) ->
            new FlightDetails(flight, airline))
                .thenCombine(reviewsFuture, (details, reviews) ->
                    new FlightDetailsWithReviews(details.getFlight(), details.getAirline(), reviews));
    }
}

Лучшие практики для асинхронных систем

  1. Логирование. Логируйте время выполнения запросов и возникающие ошибки.
  2. Тестирование. Тестируйте асинхронные вызовы с моками (Mock) и заглушками.
  3. Мониторинг. Используйте метрики и трассировку (например, Spring Sleuth) для анализа работы системы.

CompletableFuture.runAsync(() -> logger.info("Начали асинхронный запрос"));

Эти подходы помогут вам сделать ваши приложения более производительными и устойчивыми даже при высокой нагрузке.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ