Optimizar Consultas con DataLoader y Caching

Lectura
20 min~6 min lectura

Concepto clave

En GraphQL, cada campo de un tipo puede tener su propio resolver, lo que significa que una sola consulta puede desencadenar multiples llamadas a la base de datos o APIs externas. Esto se conoce como el problema N+1: para una lista de N elementos, haces 1 consulta para obtener la lista y luego N consultas mas para obtener datos relacionados de cada elemento.

DataLoader es una utilidad creada por Facebook que soluciona este problema mediante el batching y caching. Imagina que eres un camarero en un restaurante: en lugar de ir a la cocina por cada pedido individual (N+1), tomas todos los pedidos de la mesa (batching) y los llevas juntos a la cocina. Luego, si alguien pide lo mismo otra vez, ya lo tienes en tu bandeja (caching) y no necesitas volver a la cocina.

El caching en GraphQL va mas alla de DataLoader. Apollo Server incluye un cache integrado que puede almacenar respuestas completas o parciales, reduciendo la carga en tus resolvers. Combinar DataLoader para optimizar consultas a la base de datos con el cache de Apollo para respuestas HTTP es como tener un sistema de entrega express: agrupas paquetes similares y guardas los frecuentes para entregas instantaneas.

Como funciona en la practica

Vamos a implementar DataLoader en un resolver de GraphQL con Apollo Server. Supongamos que tenemos un tipo User y un tipo Post, donde cada post tiene un autor (usuario). Sin DataLoader, una consulta para obtener posts con sus autores generaria una consulta a la base de datos por cada autor.

Paso 1: Instala DataLoader en tu proyecto de Next.js con Apollo Server:

npm install dataloader

Paso 2: Crea un DataLoader para usuarios. En tu archivo de resolvers, define una funcion que cargue usuarios por sus IDs en batch:

const DataLoader = require('dataloader');

const batchUsers = async (userIds) => {
  // Supongamos que tenemos una funcion getUsersByIds que acepta un array de IDs
  const users = await getUsersByIds(userIds);
  // DataLoader espera que devuelvas los usuarios en el mismo orden que los IDs
  return userIds.map(id => users.find(user => user.id === id));
};

const userLoader = new DataLoader(batchUsers);

Paso 3: Usa el DataLoader en tu resolver. En el resolver para el campo author de Post, en lugar de hacer una consulta individual, usa el loader:

Post: {
  author: async (post) => {
    return userLoader.load(post.authorId);
  }
}

Paso 4: Configura el cache de Apollo Server. En tu configuracion de Apollo Server, puedes habilitar el cache por defecto o personalizarlo:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  cache: new InMemoryCache(),
  // Otras configuraciones...
});

Con esto, si una consulta pide multiples posts del mismo autor, DataLoader agrupara las llamadas y el cache de Apollo podra reutilizar respuestas si son identicas.

Caso de estudio

Imagina que estas construyendo una API para una plataforma de blogs con Next.js y Apollo Server. Tienes los siguientes tipos GraphQL:

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
  user: User!
}

Una consulta tipica podria ser:

query {
  posts {
    id
    title
    author {
      id
      name
    }
    comments {
      id
      text
      user {
        id
        name
      }
    }
  }
}

Sin optimizacion, esto generaria:

  • 1 consulta para obtener todos los posts
  • N consultas para los autores de cada post (donde N es el numero de posts)
  • M consultas para los usuarios de cada comentario (donde M es el numero total de comentarios)

Con DataLoader y caching:

  1. Crea un DataLoader para usuarios que cargue por IDs en batch.
  2. En los resolvers de author en Post y user en Comment, usa userLoader.load(id).
  3. Configura Apollo Server con InMemoryCache para cachear respuestas de consultas completas.

Resultado: En lugar de 1+N+M consultas, tendras 1 consulta batch para usuarios (agrupando todos los IDs unicos) y el cache reducira llamadas repetidas. Para 10 posts con 5 comentarios cada uno, pasarias de ~61 consultas a solo 2-3, mejorando el rendimiento significativamente.

Errores comunes

1. No mantener el orden en DataLoader: DataLoader requiere que el array devuelto por la funcion batch tenga el mismo orden que los IDs de entrada. Si no lo haces, asociaras datos incorrectos. Solucion: Usa un mapa o logica de busqueda que preserve el orden, como en el ejemplo del paso 2.

2. Olvidar limpiar el cache: En entornos de desarrollo, el cache puede almacenar datos obsoletos si no se invalida cuando los datos cambian. Solucion: Usa tecnicas como cache invalidation con versiones o TTL (Time To Live), o en Apollo, considera resetear el cache en mutaciones relevantes.

3. Sobrecargar el cache con datos grandes: Cachear respuestas muy grandes puede consumir mucha memoria. Solucion: Configura limites en InMemoryCache o usa estrategias de cache por fragmentos, cacheando solo campos frecuentes.

4. No usar batching en resolvers anidados: Si tienes resolvers complejos con multiples niveles, podrias perder la oportunidad de agrupar llamadas. Solucion: Disena tus DataLoaders para manejar diferentes tipos de datos y usalos consistentemente en toda la API.

5. Ignorar el contexto de ejecucion: En GraphQL con suscripciones, el cache puede no ser adecuado para datos en tiempo real. Solucion: Para suscripciones, considera deshabilitar el cache o usar cache por sesion, y combina con DataLoader para consultas batch.

Checklist de dominio

  • Identificar consultas N+1 en tu API GraphQL usando herramientas como Apollo Studio o logging.
  • Implementar al menos un DataLoader para un tipo de dato comun, como usuarios o productos.
  • Configurar Apollo Server con InMemoryCache y probar su efecto en consultas repetidas.
  • Escribir tests que verifiquen que DataLoader reduce el numero de llamadas a la base de datos.
  • Manejar escenarios donde el cache debe invalidarse, como despues de mutaciones.
  • Integrar DataLoader en resolvers anidados para optimizar consultas complejas.
  • Medir el rendimiento antes y despues de la optimizacion con metricas como tiempo de respuesta y uso de CPU.

Optimizar una API de E-commerce con DataLoader y Caching

En este ejercicio, optimizaras una API GraphQL para una tienda online usando DataLoader y el cache de Apollo Server. Sigue estos pasos:

  1. Configura el entorno: Crea un proyecto Next.js con Apollo Server. Define tipos GraphQL para Product (con id, nombre, precio) y Order (con id, productos como array de IDs de producto, usuarioId).
  2. Simula el problema N+1: Escribe resolvers basicos que, para una consulta de ordenes con productos, hagan una consulta a la base de datos por cada producto. Usa datos mock en memoria para simular la base de datos.
  3. Implementa DataLoader: Crea un DataLoader para productos que cargue por IDs en batch. Modifica el resolver de productos en Order para usar productLoader.load(id).
  4. Configura el cache: Habilitar InMemoryCache en Apollo Server. Escribe una consulta que pida multiples ordenes con los mismos productos y verifica que el cache reduce llamadas.
  5. Prueba y mide: Agrega logging para contar llamadas a la funcion batch. Ejecuta consultas antes y despues de la optimizacion, comparando el numero de llamadas y el tiempo de respuesta.
Pistas
  • Usa un objeto en memoria para simular la base de datos, por ejemplo, un array de productos con IDs unicos.
  • En la funcion batch de DataLoader, asegurate de devolver los productos en el mismo orden que los IDs de entrada.
  • Para probar el cache, ejecuta la misma consulta dos veces y verifica que la segunda sea mas rapida o tenga menos logs.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.