🐙

NestJS+Fastify+MercuriusにdataLoaderを入れる

2023/09/24に公開

graphqlのN+1問題対応で、DataLoaderを入れ時に、少しハマってしまったので、書き残しておきます。

DataLoaderについての詳しい説明は割愛します。
https://github.com/graphql/dataloader

graphql/dataloader はアプリケーションのデータ取得に使用される汎用的なユーティリティであり、データソースからのデータ取得を Batch 処理したり結果を Cache するための簡単な API を提供する。 これにより、データ取得リクエストを大幅に効率化することができる

-- by Google Bard


graphq schema👇

graphql.schema
Query {
 feeds(): Feeds
}

type Feeds {
  id: String!
  title: String!
  images(take: Int): [FeedImage!]!
}

resolver👇

feed.resolver.ts
@Resolver('Feeds')
export class FeedResolver {
  
  @Query(() => Object, {
    name: 'feeds',
    nullable: false
  })
  async feeds(){
    // feeds取得するロジック
  }
  
  @ResolveField('images')
  async feedImages(
    @Parent() feed: Feed
  ) {
  // ここは後程dataLoaderの呼び出しに変えます
   return null;
  }
}

dataloaderをインストールします。

yarn add dataloader

moduleファイル👇

app.module.ts
// ファイル分けずにここに書いちゃいます。
@Injectable(
  /** 
  * ここにscopeでrequestの指定ができますが
  * それを指定するとGraphqlModuleがネストが深くなるせいで初期化されないみたいです
  * それで結構ハマりました 
  **/
)
class DataLoaders {
  constuctor(
   @Inject() feedService: FeedService
  ){}
  
  getAllLoaders() {
    return {
     feedImage: new DataLoader<string, FeedImage>((feedIds) => {
       const images = this.feedService.getFeedImages(feedIds);
       return feedIds.map(id => images.filter(img => img.feedId === id));
     })
    }
  }
}

@Module({
  exports: [DataLoaders]
  provider: [DataLoaders]
})
class DataLoaderModule{}


@Module({
  imports: [
    GraphqlModule.forRootAsync({
      driver: MercuriusDriver
      useFactory: (dataLoaders: DataLoaders) => ({
       // ほかのオプションは省略
       context: (req: FastifyRequest, reply: FastifyReply) => ({
	 // graphqlでのHTTP contextの中にloaderを含んでしまいます。
         req, reply, dataLoaders: dataLoaders.getAllLoaders()
       })
      }),
      imports: [DataLoaderModule],
      inject: [DataLoaders]
    })
  ]
})
export class AppModule {}

resolverの修正👇

feed.resolver.ts

@Resolver('Feeds')
export class FeedResolver {
  @Query(() => Object, {
    name: 'feeds',
    nullable: false
  })
  async feeds(){
    // feeds取得するロジック
  }
  
  @ResolveField('images')
  async feedImages(
    @Parent() feed: Feed,
    @Context() context: { dataLoaders: any // <- ここはちゃんとtypeを定義したほうがいいです。 }
  ) {
-  // ここは後程dataLoaderの呼び出しに変えます
-   return null;
+   return await context.dataLoaders.feedImage.load(feed.id);
  }
}

で以上です。

importは省略しました🙇♂️
もう少し賢い書き方もありそうですが、とりあえずこれで動かすことができたので、一旦これでいきます。

Discussion