CodeGym /Cursos /Módulo 5. Spring /Lección 294: GraphQL en sistemas asíncronos

Lección 294: GraphQL en sistemas asíncronos

Módulo 5. Spring
Nivel 16 , Lección 3
Disponible

En la vida real, los datos no siempre están en una sola base de datos o en un solo servicio. A veces hay que enviar peticiones a varios microservicios o bases de datos simultáneamente, combinar los resultados y responder al cliente. Las llamadas asíncronas en GraphQL son un mecanismo que permite manejar esas tareas de forma eficiente, sin bloquear el hilo principal de ejecución.

Vamos a verlo con un ejemplo sencillo: imagina que estás desarrollando una aplicación para reservar billetes de avión. El cliente pide información del vuelo, además de reseñas y la puntuación de la aerolínea. Las reseñas y la puntuación están en una base de datos, y los datos del vuelo vienen de una API externa. Llamar a todos esos servicios de forma secuencial hace que la petición vaya lenta. El procesamiento asíncrono permite lanzar todas las peticiones en paralelo.


¿Cómo soporta GraphQL la asincronía?

Spring GraphQL admite llamadas asíncronas gracias a CompletableFuture de Java. Es la herramienta estándar para trabajar con objetos Future, que permite organizar cadenas de tareas asíncronas y manejar sus resultados de forma transparente.

Las operaciones asíncronas en GraphQL se organizan a nivel de los Data Fetchers (manejadores de datos). En lugar de devolver el resultado inmediatamente, un Data Fetcher devuelve un objeto CompletableFuture que se completa con los datos tras finalizar la ejecución asíncrona.


Data Fetchers asíncronos en Spring GraphQL

Ejemplo de un Data Fetcher asíncrono básico

Veamos cómo implementar un manejador asíncrono sencillo en Spring GraphQL. En tu aplicación añadiremos un Data Fetcher que obtiene datos de una API externa.


@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");
        // Llamada asíncrona al servicio para obtener datos del vuelo
        return CompletableFuture.supplyAsync(() -> flightService.getFlightById(flightId));
    }
}

Aquí usamos CompletableFuture.supplyAsync para obtener datos de forma asíncrona. Es el método estándar de Java para ejecutar una tarea en un hilo aparte.

Integración del Data Fetcher asíncrono en el esquema GraphQL

Ahora creemos el esquema GraphQL que invoca al Fetcher asíncrono:


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

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

Y registramos nuestro Fetcher en 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));
    }
}

Llamadas asíncronas en cadena

La asincronía es aún más útil cuando hay que combinar varias peticiones. Por ejemplo, para pedir información del vuelo y de la aerolínea a la que pertenece.


@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));
    }
}

Ahora usamos la combinación asíncrona de datos:


@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())));

        // Unimos los datos del vuelo y la aerolínea en un único objeto
        return flightFuture.thenCombine(airlineFuture, (flight, airline) -> {
            return new FlightDetails(flight, airline);
        });
    }
}

En este ejemplo se usa el método thenCombine, que combina dos CompletableFuture y crea el objeto final FlightDetails.


Enfoques para gestionar la asincronía

  1. Errores y timeouts. En sistemas asíncronos hay que prever el manejo de errores, sobre todo si la petición va a APIs externas. Lo mejor es envolver la llamada en un try-catch y devolver valores predefinidos en caso de fallo.
  2. Optimización del rendimiento. Usa pools de hilos (ThreadPoolExecutor) para limitar el número de hilos concurrentes. Esto protegerá tu sistema de sobrecarga.
  3. Dependencias complejas. Si una petición depende de otra, intenta minimizar la secuencia de llamadas. Por ejemplo, combina los datos a nivel de base de datos si es posible.

Práctica: Crear peticiones asíncronas complejas

Supongamos que tenemos las siguientes entidades:

  • Flight (información del vuelo).
  • Airline (información de la aerolínea).
  • Reviews (reseñas de la aerolínea).

Tarea: crear una consulta GraphQL que devuelva el vuelo, la aerolínea y las reseñas en una sola llamada.

En el esquema GraphQL:


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

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

Implementación del 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));
    }
}

Mejores prácticas para sistemas asíncronos

  1. Registro. Registra el tiempo de ejecución de las peticiones y los errores que surjan.
  2. Pruebas. Prueba las llamadas asíncronas con mocks (Mock) y stubs.
  3. Monitorización. Usa métricas y trazabilidad (por ejemplo, Spring Sleuth) para analizar el funcionamiento del sistema.

CompletableFuture.runAsync(() -> logger.info("Empezamos la petición asíncrona"));

Estos enfoques te ayudarán a que tus aplicaciones sean más eficientes y resistentes incluso bajo alta carga.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION