Skip to content

Commit 4360e1f

Browse files
Dilukaclaude
andcommitted
feat: add DataLoader support to reference resolver to solve n+1 query problem
- Add ReferenceLoader that batches reference queries using service.query - Update reference resolver to use DataLoader when ExecutionContext is available - Maintain backward compatibility by falling back to getById when context is missing - Batch multiple reference queries into single database query to eliminate n+1 problem 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9a6145f commit 4360e1f

File tree

3 files changed

+69
-5
lines changed

3 files changed

+69
-5
lines changed

packages/query-graphql/src/loader/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './count-relations.loader'
33
export * from './dataloader.factory'
44
export * from './find-relations.loader'
55
export * from './query-relations.loader'
6+
export * from './reference.loader'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Class, Filter, QueryService } from '@ptc-org/nestjs-query-core'
2+
3+
import { NestjsQueryDataloader } from './relations.loader'
4+
5+
export type ReferenceArgs = {
6+
id: string | number
7+
}
8+
9+
export class ReferenceLoader<DTO> implements NestjsQueryDataloader<DTO, ReferenceArgs, DTO | undefined | Error> {
10+
constructor(readonly DTOClass: Class<DTO>) {}
11+
12+
public createLoader(service: QueryService<DTO, unknown, unknown>) {
13+
return async (args: ReadonlyArray<ReferenceArgs>): Promise<(DTO | undefined | Error)[]> => {
14+
// Extract all unique IDs from the batch
15+
const ids = args.map((arg) => arg.id)
16+
17+
// Use batch query to fetch all entities at once
18+
const entities = await service.query({
19+
filter: { id: { in: ids } } as unknown as Filter<DTO>
20+
})
21+
22+
// Create a map for fast lookup by ID
23+
const entityMap = new Map<string | number, DTO>()
24+
if (entities) {
25+
entities.forEach((entity) => {
26+
const id = (entity as Record<string, unknown>).id as string | number
27+
entityMap.set(id, entity)
28+
})
29+
}
30+
31+
// Return results in the same order as requested
32+
return args.map((arg) => entityMap.get(arg.id) || undefined)
33+
}
34+
}
35+
}

packages/query-graphql/src/resolvers/reference.resolver.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { BadRequestException } from '@nestjs/common'
2-
import { Resolver, ResolveReference } from '@nestjs/graphql'
1+
import { BadRequestException, ExecutionContext } from '@nestjs/common'
2+
import { Context, Resolver, ResolveReference } from '@nestjs/graphql'
33
import { Class, QueryService } from '@ptc-org/nestjs-query-core'
44

55
import { getDTONames } from '../common'
6+
import { InjectDataLoaderConfig } from '../decorators/inject-dataloader-config.decorator'
67
import { RepresentationType } from '../federation'
8+
import { DataLoaderFactory, ReferenceLoader } from '../loader'
9+
import { DataLoaderOptions } from '../pipes/inject-data-loader-config.pipe'
710
import { BaseServiceResolver, ResolverClass, ServiceResolver } from './resolver.interface'
811

912
export interface ReferenceResolverOpts {
@@ -21,17 +24,42 @@ export const Referenceable =
2124
return BaseClass
2225
}
2326
const { key } = opts
27+
const { baseName } = getDTONames(DTOClass)
28+
const loaderName = `loadReference${baseName}`
29+
const referenceLoader = new ReferenceLoader<DTO>(DTOClass)
2430

2531
@Resolver(() => DTOClass, { isAbstract: true })
2632
class ResolveReferenceResolverBase extends BaseClass {
2733
@ResolveReference()
28-
async resolveReference(representation: RepresentationType): Promise<DTO> {
34+
async resolveReference(
35+
representation: RepresentationType,
36+
@Context() context?: ExecutionContext,
37+
@InjectDataLoaderConfig()
38+
dataLoaderConfig?: DataLoaderOptions
39+
): Promise<DTO> {
2940
const id = representation[key]
3041
if (id === undefined) {
31-
throw new BadRequestException(
32-
`Unable to resolve reference, missing required key ${key} for ${getDTONames(DTOClass).baseName}`
42+
throw new BadRequestException(`Unable to resolve reference, missing required key ${key} for ${baseName}`)
43+
}
44+
45+
// If context is available, use DataLoader for batching
46+
if (context) {
47+
const loader = DataLoaderFactory.getOrCreateLoader(
48+
context,
49+
loaderName,
50+
() => referenceLoader.createLoader(this.service),
51+
dataLoaderConfig
3352
)
53+
54+
const result = await loader.load({ id: id as string | number })
55+
if (!result) {
56+
throw new BadRequestException(`Unable to find ${baseName} with ${key}: ${String(id)}`)
57+
}
58+
59+
return result
3460
}
61+
62+
// Fallback to original behavior when context is not available (for backward compatibility)
3563
return this.service.getById(representation[key] as string | number)
3664
}
3765
}

0 commit comments

Comments
 (0)