はじめに
先日に引き続きGraphQLを一から勉強し直していきます。
前回はこちら。
N+1問題
DataLoaderについて見ていく前に、N+1問題について再度確認していく。
N+1問題とは、データベースからデータを取得する際に、1つのメインクエリに加えて、関連するデータを取得するためにN個の追加クエリが発行される問題である。これにより、パフォーマンスが大幅に低下してしまう。
例として、ユーザーとユーザーに紐づく投稿を取得する場合を考える。
最初に全ユーザーを取得し(1つのクエリ)、その後各ユーザーの投稿を取得するためにユーザーごとクエリが発行されるため(N人のユーザーがいるとして、N個のクエリ)、全部でN+1個のクエリが発行される。
query {
users {
id
name
posts {
id
title
}
}
}
DataLoader
N+1問題を解決する方法がDataLoaderである。
DataLoaderを使うことで、以下の点からバックエンドへのリクエストを減らし、パフォーマンスを向上してくれる。
- バッチ処理: 複数のデータ取得リクエストを1つのバッチにまとめて処理することで、バックエンドへのリクエスト数を減らす
- キャッシング: 同じデータに対する複数のリクエストをキャッシュすることで、重複するデータ取得を避ける
以下はGraphQL、Nest.js、prismaを使ってDataLoaderを実装した例である。
(DataLoaderに大きく関係してくる箇所以外は省略している)
prisma
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
userId Int
user User @relation(fields: [userId], references: [id])
}
graphql
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
}
type Query {
users: [User!]!
}
Resolver
import { Resolver, Query, ResolveField, Parent } from '@nestjs/graphql';
import { UsersService } from './users.service';
import * as DataLoader from 'dataloader';
@Resolver('User')
export class UsersResolver {
private postLoader: DataLoader<number, any[]>;
constructor(private readonly service: UsersService) {
// DataLoaderから内部のキューに溜まった全てのキーを渡されて処理をする
this.postLoader = new DataLoader(async (userIds: number[]) => {
const posts = await this.service.findPostsByUserIds(userIds);
return posts;
});
}
@Query('users')
async getUsers() {
return this.service.findAllUsers();
}
@ResolveField('posts')
async getPosts(@Parent() user) {
const { id } = user;
return this.postLoader.load(id); // キー(id)を内部にキューイング
}
}
Service
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findAllUsers() {
return this.prisma.user.findMany();
}
async findPostsByUserIds(userIds: number[]) {
const posts = await this.prisma.post.findMany({
where: { userId: { in: userIds } },
});
return userIds.map(userId => posts.filter(post => post.userId === userId));
}
}