https://zenn.dev/alea12/articles/15d73282c3aacc
DataLoader
を実装する。ライブラリを使えばそんなに大変ではないJOIN
での解決も可能だが、 GraphQL の道を踏み外している(ように感じる)GraphQL で 1:N のデータ構造をクエリすると、すぐに N+1 問題に行き当たります。User
と Post
が 1:N の関係となる、以下の例を見てみましょう。
type User {
id: Int!
name: String!
posts: [Post!]!
}
type Post {
id: Int!
title: String!
user: User!
}
type Query {
users: [User!]!
}
Apollo Server と TypeORM を使って素朴に Resolver を実装すると、以下のようになります。
export const resolvers: Resolvers = {
User: {
posts: async (parent: User) => await postsOfUser(parent),
},
Query: {
users: async () => await users(),
},
}
const postsOfUser = async (user: User): Promise<Post[]> => {
const postRepository = getConnection().getRepository(Post)
return await postRepository.find({ where: { user } })
}
const users = async (): Promise<User[]> => {
const userRepository = getConnection().getRepository(User)
return await userRepository.find()
}
この実装に対し、以下のようなクエリを実行することを考えます。
query ExampleQuery {
users {
id
name
posts {
id
title
}
}
}
すると、Resolver はまず users()
を実行して User
を全件取得します。その後、それぞれの user
に対して postsOfUser(user)
を実行します。その結果、データベースへのアクセスは以下のようになります。
本例では User
が 5 件登録されていましたが、ユーザーを全件取得するために 1 回、それぞれの User
に対して posts
を取得するために各 1 回、合計 6 回の SELECT
が発生しました。これが N+1 問題です[1]。
データ量が少ないうちは問題に気付きにくいですが、データ量が増えた場合や、1:N が多重階層となった場合 (例えば上記に加え Post
:Comment
= 1:N となる Comment
を考える場合) などでは、パフォーマンスに与える影響が爆発的に大きくなります。
幸い、この N+1 問題を回避する方法はいくつかあります。上記の例を ベースケース として、それぞれの解決方法を紹介します。
Facebook により提供されている DataLoader というライブラリを使う方法です。DataLoader には Batching と呼ばれる機能があり、同一テーブルに対する複数の SELECT
を 1 本にまとめてくれます。これを使うと、 Resolver の実装は以下のようになります。
export const resolvers: Resolvers = {
User: {
posts: async (parent: User) => await postsLoader.load(parent.id),
},
Query: {
users: async () => await users(),
},
}
const users = async (): Promise<User[]> => {
const userRepository = getConnection().getRepository(User)
return await userRepository.find()
}
const postsLoader = new DataLoader(async (keys): Promise<Post[][]> => {
const postRepository = getConnection().getRepository(Post)
const posts = await postRepository.find({ user: { id: In(keys as number[]) } })
return keys.map((userId) => posts.filter((post) => post.userId! === userId))
})