如何在@nestjs/graphql中优化ResolverProperty实现批量关联查询?
解决NestJS GraphQL中ResolveProperty的N+1查询问题
你遇到的是GraphQL里非常常见的N+1查询问题:当查询多个Course并关联它们的Professions时,默认的@ResolveProperty会为每个Course实例单独发起一次数据库查询,带来不必要的性能开销。要实现类似数据库左连接的"一次性查询再关联"效果,DataLoader是标准解决方案,它能批量处理这些关联查询,把N次查询合并成1次。
步骤1:安装DataLoader
首先安装DataLoader依赖:
npm install dataloader # 或者用yarn yarn add dataloader
步骤2:创建Profession的DataLoader
我们需要创建一个DataLoader服务,用来批量加载所有Course对应的Professions:
import { Injectable } from '@nestjs/common'; import * as DataLoader from 'dataloader'; import { Profession } from './profession.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { In } from 'typeorm'; @Injectable() export class ProfessionLoader { constructor( @InjectRepository(Profession) private readonly professionRepo: Repository<Profession>, ) {} createLoader() { return new DataLoader<number, Profession[]>(async (courseIds) => { // 一次性查询所有关联的Professions,传入所有Course的ID const allProfessions = await this.professionRepo.find({ where: { courseId: In(courseIds) }, // 这里假设Profession用courseId关联Course的id,根据你的实体调整字段名 }); // 把查询结果按Course ID分组 const professionMap = new Map<number, Profession[]>(); allProfessions.forEach(profession => { const courseId = profession.courseId; if (!professionMap.has(courseId)) { professionMap.set(courseId, []); } professionMap.get(courseId).push(profession); }); // 按照传入的Course ID顺序返回对应的Professions数组,没有的返回空数组(模拟左连接效果) return courseIds.map(id => professionMap.get(id) || []); }); } }
步骤3:在Resolver中使用DataLoader
修改你的CourseResolver,注入ProfessionLoader并替换原有的professions方法:
import { Resolver, Query, Args, ResolveProperty, Parent, Inject } from '@nestjs/graphql'; import { Course } from './course.entity'; import { CourseSearchDto } from './course-search.dto'; import { Profession } from '../profession/profession.entity'; import { ProfessionLoader } from '../profession/profession.loader'; @Resolver(() => Course) export default class CourseResolver { constructor( @Inject(ProfessionLoader) private readonly professionLoader: ProfessionLoader, ) {} @Query(() => [Course]) course(@Args('where', { nullable: true }) where: CourseSearchDto) { return Course.find(where); } @ResolveProperty(() => [Profession]) async professions(@Parent() course: Course) { // 使用DataLoader批量加载当前Course对应的Professions return this.professionLoader.createLoader().load(course.id); } }
步骤4:注册Loader服务
别忘了在你的模块中注册ProfessionLoader,并导出它让CourseResolver可以注入:
// profession.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Profession } from './profession.entity'; import { ProfessionLoader } from './profession.loader'; @Module({ imports: [TypeOrmModule.forFeature([Profession])], providers: [ProfessionLoader], exports: [ProfessionLoader], // 导出给其他模块使用 }) export class ProfessionModule {}
原理说明
DataLoader会自动收集同一请求中所有的load调用,把这些Course ID合并成一个数组,然后发起一次批量查询。查询完成后,它会把结果映射回每个对应的Course实例,这样就实现了类似数据库左连接的效果——只执行一次关联查询,再将结果关联到对应的Course上,彻底解决N+1问题。
另外,DataLoader默认会缓存请求结果,如果你的数据实时性要求很高,可以在创建Loader时关闭缓存:
return new DataLoader<number, Profession[]>(async (courseIds) => { // ... 原有逻辑 }, { cache: false });
内容的提问来源于stack exchange,提问作者Atzi
相关产品推荐
相关产品推荐

