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 и число запросов.