advanced

Resolvers, N+1 и DataLoader

Держите resolvers thin, предотвращайте N+1 data access, batch/cache per request и сохраняйте authorization boundaries.

Resolvers сопоставляют поля schema с данными. Наивные DB-вызовы на поле дают **N+1**: один запрос для N родителей плюс N дочерних.

					// N+1: customer на каждый order
const resolvers = {
  Query: {
    orders: () => db.order.findMany(),
  },
  Order: {
    customer: (order) => db.customer.findById(order.customerId), // N раз
  },
};
				

**DataLoader** батчит и кэширует загрузки в рамках запроса:

					const customerLoader = new DataLoader(async (ids) => {
  const customers = await db.customer.findByIds(ids);
  const map = new Map(customers.map((c) => [c.id, c]));
  return ids.map((id) => map.get(id) ?? null);
});

const resolvers = {
  Order: {
    customer: (order, _args, ctx) => ctx.loaders.customer.load(order.customerId),
  },
};
				

Рекомендации для resolvers:

  • Тонкие resolvers; бизнес-логика в services.
  • Loaders на request context (без утечек cross-request cache).
  • Авторизация в parent resolver или field middleware; не тяните данные до auth без необходимости.
  • DataLoader на уровне query или JOIN для list+relation.

На интервью: обнаружение N+1 (логи, tracing), семантика batching, почему cache DataLoader request-scoped.

Типовые ошибки: глобальный singleton DataLoader, auth только в UI, внутренние ORM entities с lazy relations, нет batch для полиморфных связей.

Компромисс: простота resolvers против явного fetching — DataLoader + service layer лучше fat ORM magic.

Чеклист:

  • Один loader instance на request.
  • Batch по key; сохраняйте порядок результатов.
  • Auth до дорогих fetch где возможно.
  • Трассируйте глубину resolver и число запросов.