PrismaのFluentAPIでN+1問題に対応する
はじめに
PrismaでGraphQL APIなどの開発をしていると、N+1問題に遭遇することがあると思います.
今回は、PrismaのFluentAPIを使ってN+1問題に対応する方法を紹介します.
N+1問題やPrismaについては本記事では省略します
1. 問題の再現
サンプルリポジトリを使います
お手元の環境でgit clone
してREADMEに従ってセットアップを行えばOKです
実際にサーバーを起動して問題を再現してみます
bun start
でサーバーを起動したら、簡単なクエリを投げてみます
{
users {
id
posts {
id
}
}
}
ターミナルを見てみるとQueryのログが出力されているのがわかります
prisma:query SELECT `main`.`User`.`id`, `main`.`User`.`firstName`, `main`.`User`.`lastName`, `main`.`User`.`createdAt`, `main`.`User`.`updatedAt` FROM `main`.`User` WHERE 1=1 LIMIT ? OFFSET ?
prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId`, `main`.`Post`.`createdAt`, `main`.`Post`.`updatedAt` FROM `main`.`Post` WHERE `main`.`Post`.`authorId` IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) LIMIT ? OFFSET ?
prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId`, `main`.`Post`.`createdAt`, `main`.`Post`.`updatedAt` FROM `main`.`Post` WHERE `main`.`Post`.`authorId` = ? LIMIT ? OFFSET ?
prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId`, `main`.`Post`.`createdAt`, `main`.`Post`.`updatedAt` FROM `main`.`Post` WHERE `main`.`Post`.`authorId` = ? LIMIT ? OFFSET ?
...
prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId`, `main`.`Post`.`createdAt`, `main`.`Post`.`updatedAt` FROM `main`.`Post` WHERE `main`.`Post`.`authorId` = ? LIMIT ? OFFSET ?
prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId`, `main`.`Post`.`createdAt`, `main`.`Post`.`updatedAt` FROM `main`.`Post` WHERE `main`.`Post`.`authorId` = ? LIMIT ? OFFSET ?
大量のクエリが発行されていることがわかります
これをFluentAPIを使用して解決していきます
2. FluentAPIとは
FluentAPI(Fluent Interface)とは、オブジェクト指向APIの実装の一種です。
このAPIは、オブジェクト自体を返すメソッドを使用して、複数のメソッド呼び出しを一つの式に連鎖させることができます。
Prismaでは、FluentAPIを用いてリレーションのあるクエリを記述することができます。例えば、以下のようにfindUnique
と.posts()
をチェーンさせることで、あるUser
のすべてのPost
を取得できます。
const posts = await prisma.user.findUnique({ where: { id: parent.id } }).posts();
3. FluentAPIを使ったN+1問題の解決
実装をFluentAPIに変更します
import type { UserResolvers } from './../../types.generated';
export const User: UserResolvers = {
fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
posts: (parent, _, { prisma }) => {
/** using Fluent API */
+ return prisma.user.findUnique({ where: { id: parent.id } }).posts();
/** not using Fluent API */
- return prisma.post.findMany({ where: { authorId: parent.id } });
},
};
もう一度クエリを投げてみます
prisma:query SELECT `main`.`User`.`id`, `main`.`User`.`firstName`, `main`.`User`.`lastName`, `main`.`User`.`createdAt`, `main`.`User`.`updatedAt` FROM `main`.`User` WHERE 1=1 LIMIT ? OFFSET ?
prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId`, `main`.`Post`.`createdAt`, `main`.`Post`.`updatedAt` FROM `main`.`Post` WHERE `main`.`Post`.`authorId` IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) LIMIT ? OFFSET ?
prisma:query SELECT `main`.`User`.`id` FROM `main`.`User` WHERE `main`.`User`.`id` IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) LIMIT ? OFFSET ?
prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId`, `main`.`Post`.`createdAt`, `main`.`Post`.`updatedAt` FROM `main`.`Post` WHERE `main`.`Post`.`authorId` IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) LIMIT ? OFFSET ?
発行されるクエリが大きく減っていることがわかります
4. なんでこうなるの?
簡単に説明すると、PrismaのDataLoaderが同じパラメータと 選択セット(includeなど) を持つクエリをグループ化し、バッチ処理を行うようになっているからです.
詳細は上記の公式ドキュメントを参照してください
まとめ
この記事では、PrismaのFluentAPIを使用してN+1問題に対処する方法を解説しました.
等価な処理でも書き方1つで大きくパフォーマンスが変わるので、ぜひ使ってみてください!
Discussion