NestJS × GraphQL × gRPCにおけるN+1問題とDataloaderの活用
NestJS × GraphQLで開発していると、N+1問題に直面する場面が多くあります。
特に、ユーザー情報などを別のマイクロサービスからgRPCで取得する場合、ネットワーク越えが発生するため、この問題がさらに深刻になります。
本記事では、以下の内容を解説します。
- Dataloaderとは何か
- N+1問題の仕組みと例
- GraphQLでのリゾルバ構造
- gRPCを使う場合に注意すべきこと
- Dataloaderを使った解決方法(実装例あり)
🔍 Dataloaderとは?
Dataloader は、Facebookが開発したバッチ処理ユーティリティライブラリです。
GraphQLでよく発生する「同じフィールドに対しての繰り返しアクセス」をバッチ化し、リクエストの最適化・パフォーマンス向上が可能です。
✅ 特徴
- 同一リクエスト内での重複読み込みをバッチ化
- 順序を保持して戻す
-
.load()
で個別に呼び出しても、内部でまとめて処理される
❗ N+1問題とは?
「N+1問題」とは、1回のリクエストに対して1つの親データ取得に加えて、各親に対してN回の子データ取得が行われる問題です。
例:投稿と著者
query {
posts {
id
title
author {
id
name
}
}
}
このGraphQLクエリに対して以下のようなResolverを組んでしまうと…
@ResolveField(() => User)
async author(@Parent() post: Post) {
return this.userService.findById(post.authorId); // N回実行される
}
投稿が10件あれば、userService.findByIdが10回実行されてしまいます。
DBアクセスやAPIコールが多発し、パフォーマンスが大きく劣化します。
🏗NestJS × GraphQLの構成とデコレーター
デコレーター 説明
@Query(() => [Post]) GraphQLのpostsクエリを定義
@ResolveField(() => User) Post型の中のauthorフィールドを解決するリゾルバ
@Parent() 親オブジェクト(ここではPost)を取得
GraphQLは「必要なデータだけを取得する」ことがメリットですが、それにより裏側では複数のデータ取得処理が走る構造になりがちです。
💬SQLならJOINで解決できるけど…
例えばRDBであれば以下のようにJOINで一発取得が可能です。
SELECT posts.*, users.*
FROM posts
JOIN users ON posts.author_id = users.id;
ORMでも以下のようにincludeを使えば解決できます。
prisma.post.findMany({
include: {
author: true
}
});
しかし、GraphQLのようにリゾルバがフィールドごとに独立して定義されている場合、このJOIN的なアプローチが使いづらくなり、N+1が発生しやすくなります。
⚡gRPCを使っているときのN+1のヤバさ
今回の構成では、投稿の著者情報を別マイクロサービスからgRPCで取得しています。
❌悪い例:投稿ごとにgRPC呼び出し
@ResolveField(() => User)
async author(@Parent() post: Post) {
return this.userClient.getUserById(post.authorId); // N回 gRPC 呼び出し
}
投稿が10件あれば、gRPCで10回サービス間通信が発生します。
gRPCはDBアクセスより通信コストが高い
サービス間通信が直列に走ると遅延が増大
スケーラビリティや安定性にも悪影響
✅DataloaderでgRPCをバッチ化する
gRPCでもDataloaderを使えば、リクエストをバッチ化して1回のgRPC通信にまとめることができます。
📦UserLoaderの実装(gRPC用)
@Injectable({ scope: Scope.REQUEST })
export class UserLoader {
constructor(private readonly userClient: UserGrpcClient) {}
readonly batchUsers = new DataLoader(async (userIds: readonly string[]) => {
const users = await this.userClient.getUsersByIds(userIds as string[]); // gRPCで一括取得
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id));
});
}
📍Resolver側での利用
@ResolveField(() => User)
async author(@Parent() post: Post) {
return this.userLoader.batchUsers.load(post.authorId);
}
これにより、投稿者情報が一括で取得されるようになります。
✅まとめ
観点 内容
GraphQLの構造 フィールド単位のリゾルバでN+1が起きやすい
gRPC連携 DBよりも高コスト。N回のサービス間通信は致命的
解決策 Dataloaderを導入してバッチ処理にする
メリット パフォーマンス改善、レイテンシ削減、ネットワーク負荷軽減
🔧 補足リンク
Dataloader GitHub
NestJS公式 GraphQLドキュメント
gRPC in NestJS
📝おわりに
GraphQL × Microservice構成では、N+1問題は避けて通れない壁です。
NestJSとgRPCを組み合わせる場合は特に、Dataloaderを用いたバッチ処理による最適化をぜひ導入してみてください。
質問や補足などあれば、コメントで気軽にどうぞ!## 🧩 Dataloaderとは?
Dataloader は、GraphQLのリゾルバが引き起こすN+1問題を解決するためのユーティリティライブラリです。
- 同じフィールドへの複数アクセスをバッチ化
- データ取得をまとめて実行してパフォーマンスを最適化
// 投稿ごとにauthor情報を取得(悪い例)
@ResolveField(() => User)
async author(@Parent() post: Post) {
return this.userService.findById(post.authorId); // N回実行される
}
Discussion