Проблема N+1 и DataLoader
↑ 5.3 GraphQL на бэкенде и Federation · ← Предыдущая · → Следующая
Зачем это на собесе
Главный вопрос по GraphQL-производительности; DataLoader — стандартный ответ.
Объяснение
Запрос orders { customer { name } }: resolver customer вызывается для каждого заказа — 1 запрос на список + N запросов за клиентами.
DataLoader собирает все ключи, запрошенные в рамках одного «тика» выполнения, и выполняет один пакетный запрос; результат кэшируется в рамках запроса.
public class CustomerByIdDataLoader(IBatchScheduler scheduler, IDbContextFactory<AppDbContext> factory, DataLoaderOptions? options = null)
: BatchDataLoader<Guid, Customer>(scheduler, options ?? new DataLoaderOptions())
{
protected override async Task<IReadOnlyDictionary<Guid, Customer>> LoadBatchAsync(IReadOnlyList<Guid> keys, CancellationToken ct)
{
await using var db = await factory.CreateDbContextAsync(ct);
return await db.Customers.Where(c => keys.Contains(c.Id)).ToDictionaryAsync(c => c.Id, ct);
}
}
public async Task<Customer> GetCustomer([Parent] Order o, CustomerByIdDataLoader loader, CancellationToken ct)
=> await loader.LoadAsync(o.CustomerId, ct);Итог: 2 запроса вместо N+1: orders и customers where id in (...).
| Вид | Смысл |
|---|---|
BatchDataLoader | один ключ → одно значение |
GroupedDataLoader | один ключ → коллекция (заказы клиента) |
CacheDataLoader | кэш без пакетирования |
Нюансы и подводные камни
- DbContext не потокобезопасен: используйте
IDbContextFactoryв DataLoader. - Слишком большие
IN— ограничивайте размер батча. - При
UseProjectionчасть N+1 решается самим EF, DataLoader нужен для связей между сервисами и не-EF источников. - DataLoader кэширует в рамках запроса; не путайте с межзапросным кэшем.
Практика
- Включите логирование SQL и увидите N+1, затем внедрите DataLoader.
- Сделайте
GroupedDataLoaderдля заказов клиента. - Сравните число запросов до и после.
Вопросы с ответами
Как DataLoader решает N+1?
Накапливает ключи за тик выполнения и загружает их одним пакетным запросом.
DataLoader или Include?
Include/проекции — внутри одного источника EF; DataLoader — универсально, в том числе для других сервисов.