DataLoaders
DataLoaders help prevent N+1 query problems by batching and caching data requests within a single GraphQL request.
Overview
Section intitulée « Overview »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
How It Works
Section intitulée « How It Works »Type GraphQL Query uses a DataLoaderFactory to create and manage DataLoaders automatically for relations.
Automatic DataLoader Creation
Section intitulée « Automatic DataLoader Creation »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.
DataLoaderFactory
Section intitulée « DataLoaderFactory »The DataLoaderFactory manages DataLoaders in the GraphQL context:
import { DataLoaderFactory } from '@lib/type-graphql-query';
// Get or create a DataLoaderconst 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);Method Signature
Section intitulée « Method Signature »static getOrCreateLoader<K, V>( context: TypeGraphqlContext, name: string, createHandler: () => Dataloader.BatchLoadFn<K, V>, options?: DataLoaderOptions): Dataloader<K, V>Parameters:
context- GraphQL context (must havetypeGraphqlQueryLoadersproperty)name- Unique name for the DataLoadercreateHandler- Function that returns the batch load handleroptions- Optional DataLoader options
Returns: A DataLoader instance (reused if already exists)
Context Type
Section intitulée « Context Type »The context must extend TypeGraphqlContext:
interface TypeGraphqlContext { typeGraphqlQueryLoaders?: Record<string, Dataloader<unknown, unknown>>;}Custom DataLoaders
Section intitulée « Custom DataLoaders »You can create custom DataLoaders for specific use cases:
Example: Custom Product Loader
Section intitulée « Example: Custom Product Loader »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); }}Example: With Custom Options
Section intitulée « Example: With Custom Options »const loader = DataLoaderFactory.getOrCreateLoader( context, 'customLoader', () => async (ids: string[]) => { // Batch load }, { cache: true, maxBatchSize: 100, // Maximum batch size cacheMap: new Map(), // Custom cache },);DataLoader Options
Section intitulée « DataLoader Options »Available options (from dataloader package):
cache: boolean- Enable/disable caching (default:true)maxBatchSize: number- Maximum items per batchcacheMap: Map- Custom cache implementationcacheKeyFn: (key: K) => string- Custom cache key function
Note: Type GraphQL Query automatically uses a custom cacheKeyFn that handles bigint values correctly.
Best Practices
Section intitulée « Best Practices »- Use Automatic Relations - Let Type GraphQL Query handle DataLoaders for standard relations
- Name DataLoaders Uniquely - Use descriptive names to avoid conflicts
- Batch Efficiently - Ensure batch load functions are efficient
- Handle Errors - Return errors in the batch load function, not thrown
Error Handling
Section intitulée « Error Handling »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); } },);Performance Considerations
Section intitulée « Performance Considerations »When DataLoaders Help
Section intitulée « When DataLoaders Help »- Relations - Loading related entities
- Nested Queries - Multiple levels of nesting
- List Queries - Querying lists with relations
When DataLoaders Don’t Help
Section intitulée « When DataLoaders Don’t Help »- Single Queries - Already optimized
- Aggregations - Use database aggregations instead
- Custom Logic - Complex business logic may need different approaches
Debugging
Section intitulée « Debugging »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; },);Next Steps
Section intitulée « Next Steps »- Relations - Entity relationships
- Resolvers - Resolver implementation
- QueryService - Query service interface