Aller au contenu

DataLoaders

DataLoaders help prevent N+1 query problems by batching and caching data requests within a single GraphQL request.

DataLoaders are automatically used by Type GraphQL Query for relations. They batch multiple requests and cache results per request.

Benefits:

  • Batching - Multiple requests are batched into one database query
  • Caching - Results are cached per GraphQL request
  • Performance - Reduces database queries significantly

Type GraphQL Query uses a DataLoaderFactory to create and manage DataLoaders automatically for relations.

When you define relations, DataLoaders are automatically created:

import { Relation } from '@lib/type-graphql-query';
import { ObjectType } from '@lib/type-graphql';
import { Category } from './category.type';
@ObjectType()
export class Product {
@IDField(() => ID)
id: string;
@Relation('category', () => Category)
category: Category;
}

When querying products with categories, the DataLoader automatically batches category requests:

query {
products {
id
name
category {
id
name
}
}
}

Instead of making N queries (one per product), it makes a single batched query.

The DataLoaderFactory manages DataLoaders in the GraphQL context:

import { DataLoaderFactory } from '@lib/type-graphql-query';
// Get or create a DataLoader
const loader = DataLoaderFactory.getOrCreateLoader(
context,
'categoryLoader',
() => async (ids: string[]) => {
// Batch load function
const categories = await categoryService.findByIds(ids);
return ids.map((id) => categories.find((c) => c.id === id));
},
{ cache: true }, // Options
);
static getOrCreateLoader<K, V>(
context: TypeGraphqlContext,
name: string,
createHandler: () => Dataloader.BatchLoadFn<K, V>,
options?: DataLoaderOptions
): Dataloader<K, V>

Parameters:

  • context - GraphQL context (must have typeGraphqlQueryLoaders property)
  • name - Unique name for the DataLoader
  • createHandler - Function that returns the batch load handler
  • options - Optional DataLoader options

Returns: A DataLoader instance (reused if already exists)

The context must extend TypeGraphqlContext:

interface TypeGraphqlContext {
typeGraphqlQueryLoaders?: Record<string, Dataloader<unknown, unknown>>;
}

You can create custom DataLoaders for specific use cases:

import { DataLoaderFactory } from '@lib/type-graphql-query';
@Resolver(() => Order)
export class OrderResolver {
@FieldResolver(() => [Product])
async products(@Root() order: Order, @Ctx() context: Context): Promise<Product[]> {
const loader = DataLoaderFactory.getOrCreateLoader(
context,
'orderProducts',
() => async (orderIds: string[]) => {
// Batch load products for multiple orders
const products = await this.productService.findByOrderIds(orderIds);
return orderIds.map((id) => products.filter((p) => p.orderId === id));
},
);
return loader.load(order.id);
}
}
const loader = DataLoaderFactory.getOrCreateLoader(
context,
'customLoader',
() => async (ids: string[]) => {
// Batch load
},
{
cache: true,
maxBatchSize: 100, // Maximum batch size
cacheMap: new Map(), // Custom cache
},
);

Available options (from dataloader package):

  • cache: boolean - Enable/disable caching (default: true)
  • maxBatchSize: number - Maximum items per batch
  • cacheMap: Map - Custom cache implementation
  • cacheKeyFn: (key: K) => string - Custom cache key function

Note: Type GraphQL Query automatically uses a custom cacheKeyFn that handles bigint values correctly.

  1. Use Automatic Relations - Let Type GraphQL Query handle DataLoaders for standard relations
  2. Name DataLoaders Uniquely - Use descriptive names to avoid conflicts
  3. Batch Efficiently - Ensure batch load functions are efficient
  4. Handle Errors - Return errors in the batch load function, not thrown
const loader = DataLoaderFactory.getOrCreateLoader(
context,
'productLoader',
() => async (ids: string[]) => {
try {
const products = await productService.findByIds(ids);
return ids.map(
(id) => products.find((p) => p.id === id) || new Error(`Product ${id} not found`),
);
} catch (error) {
return ids.map(() => error);
}
},
);
  • Relations - Loading related entities
  • Nested Queries - Multiple levels of nesting
  • List Queries - Querying lists with relations
  • Single Queries - Already optimized
  • Aggregations - Use database aggregations instead
  • Custom Logic - Complex business logic may need different approaches

To debug DataLoader behavior:

const loader = DataLoaderFactory.getOrCreateLoader(
context,
'debugLoader',
() => async (ids: string[]) => {
console.log('Batch loading:', ids);
const results = await loadData(ids);
console.log('Batch results:', results);
return results;
},
);