😊

PrismaのFluentAPIでN+1問題に対応する

2023/09/25に公開

はじめに

PrismaでGraphQL APIなどの開発をしていると、N+1問題に遭遇することがあると思います.
今回は、PrismaのFluentAPIを使ってN+1問題に対応する方法を紹介します.

N+1問題やPrismaについては本記事では省略します

1. 問題の再現

https://zenn.dev/nyatinte/articles/656822f279cb37
上記の記事で作成したサンプルリポジトリを使います
https://github.com/nyatinte/bun-yoga-server-preset-test
お手元の環境でgit cloneしてREADMEに従ってセットアップを行えばOKです

実際にサーバーを起動して問題を再現してみます

sh
bun start

でサーバーを起動したら、簡単なクエリを投げてみます

localhost
{
  users {
    id
    posts {
      id
    }
  }
}

ターミナルを見てみるとQueryのログが出力されているのがわかります

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とは

https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#fluent-api

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に変更します

src/schema/user/resolvers/User.ts
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 } });
  },
};

もう一度クエリを投げてみます

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`.`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. なんでこうなるの?

https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance#solving-n1-in-graphql-with-findunique-and-prismas-dataloader

https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#fluent-api

簡単に説明すると、PrismaのDataLoaderが同じパラメータ選択セット(includeなど) を持つクエリをグループ化し、バッチ処理を行うようになっているからです.
詳細は上記の公式ドキュメントを参照してください

まとめ

この記事では、PrismaのFluentAPIを使用してN+1問題に対処する方法を解説しました.
等価な処理でも書き方1つで大きくパフォーマンスが変わるので、ぜひ使ってみてください!

GitHubで編集を提案

Discussion