У реальному житті дані не завжди живуть в одній базі даних або в одному сервісі. Іноді потрібно відправити запити до кількох мікросервісів або баз даних одночасно, об’єднати результати і відповісти клієнту. Асинхронні виклики в 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.
Підходи до управління асинхронністю
- Помилки та таймаути. В асинхронних системах треба передбачити обробку помилок, особливо якщо запит йде до зовнішніх API. Найкращий варіант — загорнути виклик у блок
try-catchі повертати заздалегідь визначені значення у випадку помилок. - Оптимізація продуктивності. Використовуйте пул потоків (
ThreadPoolExecutor), щоб обмежити кількість одночасно працюючих потоків. Це захистить вашу систему від перевантаження. - Складні залежності. Якщо один запит залежить від іншого, намагайтеся мінімізувати послідовність викликів. Наприклад, об'єднуйте дані вже на рівні бази даних, якщо це можливо.
Практика: Створення складних асинхронних запитів
Припустимо, у нас є такі сутності:
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));
}
}
Кращі практики для асинхронних систем
- Логування. Логуйте час виконання запитів і помилки.
- Тестування. Тестуйте асинхронні виклики з моками (Mock) і заглушками.
- Моніторинг. Використовуйте метрики й трасування (наприклад, Spring Sleuth) для аналізу роботи системи.
CompletableFuture.runAsync(() -> logger.info("Почали асинхронний запит"));
Ці підходи допоможуть вам зробити ваші додатки більш продуктивними та стійкими навіть при великому навантаженні.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ