🐈

Nest.jsのgraphQLでdataloaderを使う理由と使い方

2023/02/20に公開

本記事は何?

  • graphQLでdataloaderを使う理由
  • Nest.jsでdataloaderを使う方法
    を記述して、Nest.jsにdataloaderを導入し、動作確認を行えることをゴールとします。

想定する読者と環境前提

  • Nest.jsでgraphQLを利用できる環境が整っている。

本記事で例として使われるコード

スキーマ

PostにauthorとしてUserが紐づいています。
関係は、Post:User = N:1です。

type Post {
  id: ID!
  title: String!
  author: User!
}
 
type User {
  id: ID!
  name: String!
}
 
type Query {
  posts: [Post]
}

Resolver


@Resolver(of => Post)
export class PostsResolver {
  constructor(
    private readonly userService: UserService,
    private readonly postsService: PostsService,
  ) {}
 
  @Query(returns => [Post])
  posts() {
    return this.postsService.findAll();
  }
  
  @ResolveField(returns => [User])
  author(@Parent() post: Post) {
    return this.userService.findById(post.userId);
  }
}

userService, postsServiceはDBからpostレコードを取得するクラスです。
userService, postsServiceは、本記事では省略しています。

実行クエリ

query {
  posts {
    id
    title
    author { 
      id
      name
     }
  }
}

なぜ、graphQLではdataloaderを使う必要があるの?

A. N+1問題を対策するためである。

N+1問題とは

graphQLではフィールドをリゾルブするごとに、リゾルバが実行されるので、postsのデータの数だけ、Post.authorのリゾルバを実行されます。
つまり、下記のように、同じようなSQLが複数回実行されることになります。

SELECT * FROM post;
> {userId: 1 .... } {userId: 2 ... } {userId:4, ....} ....

SELECT * FROM user WHERE id = 1;
SELECT * FROM user WHERE id = 2;
SELECT * FROM user WHERE id = 2;
SELECT * FROM user WHERE id = 2;
SELECT * FROM user WHERE id = 4;
SELECT * FROM user WHERE id = 5;
SELECT * FROM user WHERE id = 9;
SELECT * FROM user WHERE id = 10;

post.userdが同値であっても、上記のように、重複してSQLが実行されます。
このようなN+1問題が発生するリゾルバでは、パフォーマンス面に影響を与えてしまいます。

これを対策するためにdataloaderを利用する必要があります。

dataloaderを使うことでどうなる?

As Is

dataloaderが導入されていないと、先述のようにSQLがリゾルブの結果分、実行されます。

To Be

dataloaderを導入することで下記のように、SQLの実行数を抑えることができる。
パフォーマンス改善に役立ちます。

SELECT * FROM post;
> {userId: 1 .... } {userId: 2 ... } {userId:4, ....} ....

SELECT * FROM user WHERE id in (1, 2, 4, 5, 9, 10); #userIdをまとめてSQL実行!

Nest.jsでのdataloaderの実装方法

実装手順

省略していますが、app.moduleへのファイルの追加も忘れずに行う。

dataloaderをnpm install

まずはnpmでdataloaderをinstallします。
https://github.com/graphql/dataloader

npm i dataloader

base.dataloaderを用意

次に、base.dataloaderというabstarctClassを作成します。

user.dataloaderを用意

次にbase.dataloaderを継承したuser.dataloaderを用意します。

@Injectable()
export class UsersDataloader extends BaseDataloader<number, User> {
  constructor(
  
    private readonly usersService: UsersService,
  ) {}
 
  // batchloadという関数名
  protected batchLoad(keys: number[]): Promise<(User | Error)[]> {
    return this.usersService.findByIds(keys);
  }
}

PostsResolverを修正


@Resolver(of => Post)
export class PostsResolver {
  constructor(
-   private readonly userService: UserService,
    private readonly postsService: PostsService,
  ) {}
 
  @Query(returns => [Post])
  posts() {
    return this.postsService.findAll();
  }
  
  @ResolveField(returns => [User])
  author(@Parent() post: Post) {
-   return this.userService.findById(post.userId);
+   return this.usersDataloader.load(post.userId);
  }
}

tsconfig.jsの修正

{
  "compilerOptions": {
    ...
    "esModuleInterop": true
  }
}

tsconfig.jscompilerOptionsesModuleInterop: true を追加してください。
この記述がないと、下記のエラーが発生して、クエリが正常終了しません。

dataloader_1.default is not a constructor

動作検証

実行されるSQLのログ出力を行って、複数結果を伴うPostクエリを実行しても、Authorを取得するために実行されるクエリは一つであることを確認できれば正です。

まとめ

今回は簡単ですが、Nest.jsでdataloderを使う方法を記述しました。
最後までご覧いただきありがとうございます。
間違っている点などお気軽にコメントいただければ幸いです!

Discussion