GraphQLにおけるN+1問題の解決策

バックエンドにあるREST APIからデータを取得して、GraphQLのスキーマに載せて返すようなBFF（Backend for Frontend）を設計しています。
その過程で、GraphQLでN+1問題を回避するにはどうしたらいいか？という課題に直面したので、今回はその解決策をまとめてみます。
## N+1問題とは何か
まず、N+1問題とは何かという話から始めます。
例えば、データベース上にAuthor（著者）テーブルとBook（本）テーブルが存在し、関連は次のようであるとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254418/194115fd-196b-0004-f26c-4b7e6d1ae5b7.png)

今回は「著者の一覧を、各著者に紐づく本の情報とともに取得できるようにしてほしい」というクライアントからの要件を受けて、次のようなGraphQLのスキーマを定義しました。

```graphql:schema.gql
type Query {
  """著者一覧取得"""
  authors: [Author!]!
}

type Author {
  books: [Book!]!

  """著者ID"""
  id: Int!

  """著者名"""
  name: String!
}

type Book {
  """著者ID"""
  authorId: Int!

  """ISBN"""
  id: String!

  """タイトル"""
  title: String!
}
```
図で描くとこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254418/9ee8c911-47bd-0890-59d7-6e7e28c34a6a.png)

そして、Queryノード内で"authors"ノードが要求されたときに動くリゾルバと、Authorノード内で"books"ノードが要求されたときに動くリゾルバを、かくかくしかじかと定義しました。（具体的な実装は[こちら](https://qiita.com/nakawodayo/items/2f2f98c9833659da66f0#%E5%AE%9F%E8%A3%85%E4%BE%8Bn1%E5%95%8F%E9%A1%8C%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%99%E3%82%8B%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0)）

さて、一通りのプログラムが完成したので、いよいよ動作確認です。GraphQLのエンドポイント（/graphql）に向けて、次のようなQueryを投げてみます。

```graphql:query.gql
  query {
           authors {
            id
            name
            books {
              id
              title
            }
          }
        }
```
すると、次のようにばっちりデータを取得することができました。めでたしめでたし。
```json:authors.json
{
  "data": {
    "authors": [
      {
        "id": 1,
        "name": "Samuel Beckett",
        "books": [
          {
            "id": "978-0802144423",
            "title": "Waiting for Godot"
          },
          {
            "id": "978-0571244577",
            "title": "Happy Days"
          }
        ]
      },
      {
        "id": 2,
        "name": "Anton Pavlovich Chekhov",
        "books": [
          {
            "id": "978-1559365512",
            "title": "The Cherry Orchard"
          }
        ]
      },
      {
        "id": 3,
        "name": "William Shakespeare",
        "books": []
      }
    ]
  }
}
```
と言いたいところなのですが、よくよくログを確認してみると、
```
GET http://localhost:8080/authors (12ms)
GET http://localhost:8080/authors/2/books (6ms)
GET http://localhost:8080/authors/3/books (8ms)
GET http://localhost:8080/authors/1/books (9ms)
```
このように、処理の過程で4回のデータ取得（/authorsに1回、/authors/{authorId}/booksに3回）が発生していることが分かりました。
今回は/authorsの返却値が3件だったので、たかだか3+1=4回のHTTPリクエストで済みました。しかし、/authorsの返却値が1,000件だったら？10,000件だったら？

このように、親ノードを取得するために1回のデータ取得が行われた後、紐づく子ノードを取得するためにN回のデータ取得を行ってしまうことを**N+1問題**といいます。

この記事では、パフォーマンスに大きな影響が出てしまうN+1問題を、GraphQLで回避するための解決策をご紹介します。

## 解決策1: JOIN FETCHする
REST APIの設計に慣れている人にとって、最初に思いつく解決策は**SQLを発行する時点でJOIN FETCHする**ことだと思います。

今回はバックエンドにあるREST APIにHTTPリクエストを送信する構成になっていますが、SQLで考えると、
```sql
SELECT * FROM Author;
SELECT * FROM Book WHERE author_id = 2;
SELECT * FROM Book WHERE author_id = 3;
SELECT * FROM Book WHERE author_id = 1;
```
のようなクエリが発行されている状態です。

これを回避するために、
```sql
SELECT * FROM Author a LEFT JOIN Book b ON a.id = b.author_id;
```
というSQLを発行するようなREST APIのエンドポイント（/authorsWithBooks）を作成し、BFF（GraphQL）からはこのエンドポイントを呼び出すようにします。

具体的な実装は次のようになります。

まず、REST APIを呼び出す処理をまとめたクラスに、/authorsWithBooksを呼び出すための関数を追加します。
```typescript:author.service.ts
  public async findAllWithBooks(): Promise<Array<Author>> {
    const data = await this.get(`http://localhost:8080/authorsWithBooks`);
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }
```
次に、"authors"というQueryが要求されたときに動くリゾルバで、この関数が呼ばれるように修正します。
```typescript:authors.resolver.ts
  @Query(() => [Author], { name: 'authors', description: '著者一覧取得' })
  public async getAuthors(): Promise<Author[]> {
    console.log('-------------getAuthors START-------------');

    // const authors = await this.authorService.findAll();
    const authors = await this.authorService.findAllWithBooks();

    console.log('-------------getAuthors END-------------');
    return authors.map((author) => new Author(author));
  }
```
最後に、"books"ノードが要求されたときに、/authors/{authorId}/booksに対してリクエストをしないように、Authorノード内のリゾルバを削除します。
```typescript:author.resolver.ts
@Resolver(() => Author)
export class AuthorResolver {
  // constructor(private bookService: BookService) {}
  // @ResolveField('books', () => [Book])
  // public async getBooks(@Parent() author: Author): Promise<Book[]> {
  //   console.log('-------------getBooks START-------------');
  //   const books = await this.bookService.findByAuthorId(author.id);
  //   console.log('-------------getBooks END-------------');
  //   return books.map((book) => new Book(book));
  // }
}
```
これでもう一度同じQueryを実行してみます。すると、今度も同じ結果を得ることができるはずです。
<details><summary>Query</summary>

```graphql:query.gql
  query {
           authors {
            id
            name
            books {
              id
              title
            }
          }
        }
```
</details>

<details><summary>レスポンス結果</summary>

```json:authors.json
{
  "data": {
    "authors": [
      {
        "id": 1,
        "name": "Samuel Beckett",
        "books": [
          {
            "id": "978-0802144423",
            "title": "Waiting for Godot"
          },
          {
            "id": "978-0571244577",
            "title": "Happy Days"
          }
        ]
      },
      {
        "id": 2,
        "name": "Anton Pavlovich Chekhov",
        "books": [
          {
            "id": "978-1559365512",
            "title": "The Cherry Orchard"
          }
        ]
      },
      {
        "id": 3,
        "name": "William Shakespeare",
        "books": []
      }
    ]
  }
}
```
</details>

では、肝心のログを見てみましょう。
```
GET http://localhost:8080/authorsWithBooks (7ms)
```
はい、今度は1回のHTTPリクエストで処理が完結しています。

### info.fieldNodesの利用
しかし、このコーディングには1つの問題点があります。

例えば、Queryの内容が次のように、Authorに直接紐づくノードのみを要求し、booksノードを要求しなかった場合、処理はどのようになるでしょうか。
```graphql:query.gql
  query {
           authors {
            id
            name
          }
        }
```
答えはもちろん、"authors"に紐づくリゾルバが動いた結果、authorService.findAllWithBooksが呼ばれるため、
```
GET http://localhost:8080/authorsWithBooks (6ms)
```
このエンドポイントにHTTPリクエストを送信することになります。

しかしこれでは、バックエンドのREST API側で**本来やる必要のないJOINが発生している**ことになり、パフォーマンスを低下させてしまいます。

この問題を解消するためには、リクエストされたQuery内で具体的にどのノードが要求されたかを見て、どのバックエンドAPIを呼び出すべきかを判断する必要があります。

各リゾルバ内で、リクエストされたQueryのツリー構造を知るためには、GraphQLの**infoオブジェクト** [^1]を利用します。

```typescript:authors.resolver.ts
import { Info, Query, Resolver } from '@nestjs/graphql';
import { Author } from './author.model';
import { AuthorService } from './author.service';

@Resolver(() => [Author])
export class AuthorsResolver {
  constructor(private authorService: AuthorService) {}

  @Query(() => [Author], { name: 'authors', description: '著者一覧取得' })
  public async getAuthors(@Info() info: any): Promise<Author[]> {
    console.log('-------------getAuthors START-------------');

    if (this.doesPathExist(info, 'books')) {
      const authors = await this.authorService.findAllWithBooks();

      console.log('-------------getAuthors END-------------');
      return authors.map((author) => new Author(author));
    } else {
      const authors = await this.authorService.findAll();

      console.log('-------------getAuthors END-------------');
      return authors.map((author) => new Author(author));
    }
  }

  /**
   *
   * @param info GraphQLのinfoオブジェクト
   * @param path 判定したいノード
   * @returns Queryにpathが含まれる場合はtrue, それ以外はfalse
   */
  private doesPathExist(info: any, path: string): boolean {
    for (const selection of info.fieldNodes[0].selectionSet.selections) {
      if (selection.name.value == path) {
        return true;
      }
    }
    return false;
  }
}
```

こうすることで、Query内に"books"が要求された場合は
```
GET http://localhost:8080/authorsWithBooks (44ms)
```
のエンドポイントが呼ばれ、要求されなかった場合は
```
GET http://localhost:8080/authors (7ms)
```
のエンドポイントが呼ばれることになるため、無駄なJOINが発生する問題は解消することができました。

## 解決策2: 幅優先（breadth-first）でフェッチする
上記の解決策も悪くはないものの、「1つのノードに1つの対応するリゾルバを割り当てる」というGraphQLの基本的な考え方からは外れてしまっています。つまり、「複数のノードに関するロジックを1つのリゾルバ内で実行している」点が、あまりGraphQLらしくないと言えます。[^2]

では、他にどのような解決のアプローチがあるのでしょうか。
もう一度、N+1問題が発生する原因をグラフを用いて考えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254418/c06b79da-281d-2f8d-16a5-fa6eba992343.png)

上図のように、"authors"に紐づくN件の"Author"を取得し、さらに"Author"の子ノードである"books"を取得し......という具合に、深さ優先（depth-first）の考え方で処理をしていくと、どうしても親ノードの数だけクエリが発生してしまいます。

しかし、とある賢人は考えました。
幅優先（breadth-first）でレイヤーごとにデータ取得をするようにすれば、「クエリの数＝ネストの数」に抑えることができるのではないか？（つまり、N+1問題を1+1+......にできる！）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254418/c8bf8149-b2d4-6660-c145-9419a81cdc74.png)

図にするとこんな感じです。ポイントは、今まで
```sql
SELECT * FROM Book WHERE author_id = :authorId;
```
だったクエリが、
```sql
SELECT * FROM Book WHERE author_id IN (:authorIds);
```
とWHERE IN句を用いたSQLに変わっているところです。

つまり、全てのAuthorのauthorId（=BookのPK）が出揃ったタイミングで、バッチ的にbooksを一括取得するという考え方です。

そして、これを可能にするのが、Facebookのエンジニアが開発した**Dataloader**というライブラリです。
https://github.com/graphql/dataloader

### Dataloaderを用いた実装
まず、バックエンドのREST APIに
```sql
SELECT * FROM Book WHERE author_id IN (:authorIds);
```
に対応するエンドポイント（/books/findByAuthorIds）を作成します。

そして、REST APIを呼び出す処理をまとめたクラスに、/books/findByAuthorIdsを呼び出すための関数を追加します。

```typescript:book.service.ts
  public async findByAuthorIds(authorIds: string[]): Promise<Array<Book>> {
    const idsString = authorIds.join(',');
    const data = await this.get(`http://localhost:8080/books/findByAuthorIds`, {
      params: {
        authorIds: idsString,
      },
    });
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }
```
ここで、AuthorResolverから直接上記のfindByAuthorIds()を呼び出すのではなく、間にDataloaderを噛ませます。

<details><summary>base-dataloader.ts</summary>

```typescript:base-dataloader.ts
import * as DataLoader from 'dataloader';

export abstract class BaseDataloader<K, V> extends Object {
  protected dataloader: DataLoader<K, V> = new DataLoader<K, V>(
    this.batchLoad.bind(this),
  );

  public clear(key: K): DataLoader<K, V> {
    return this.dataloader.clear(key);
  }

  public clearAll(): DataLoader<K, V> {
    return this.dataloader.clearAll();
  }

  public async load(key: K): Promise<V> {
    return this.dataloader.load(key);
  }

  public async loadMany(keys: K[]): Promise<(V | Error)[]> {
    return this.dataloader.loadMany(keys);
  }

  public prime(key: K, value: V): DataLoader<K, V> {
    return this.dataloader.prime(key, value);
  }

  protected abstract batchLoad(keys: K[]): Promise<(V | Error)[]>;
}

```
</details>

```typescript:book.dataloader.ts
import { BaseDataloader } from '@/graphql/base-dataloader';
import { Injectable, Scope } from '@nestjs/common';
import { Book } from './book.backend.model';
import { BookService } from './book.service';

@Injectable({ scope: Scope.REQUEST })
export class BookDataloader extends BaseDataloader<string, Book[]> {
  constructor(private readonly bookService: BookService) {
    super();
  }

  protected async batchLoad(keys: string[]): Promise<Array<Book[]>> {
    console.log('-------------BookDataloader batchLoad-------------');

    const books = await this.bookService.findByAuthorIds(keys);

    // authorId単位でbooksを仕分ける
    const groupedByAuthorId = this.groupByAuthorId(keys, books);

    return keys.map((key) => groupedByAuthorId[key]);
  }

  /**
   *
   * @param keys 親ノードAuthorのauthorIdの配列
   * @param books バックエンドAPIから取得したBookの配列
   * @returns key: authorId, value: Book[]のMap
   */
  private groupByAuthorId(
    keys: string[],
    books: Book[],
  ): Record<string, Book[]> {
    return Object.fromEntries(
      keys.map((key) => [
        key,
        books.filter((book) => key === book.authorId.toString()),
      ]),
    );
  }
}
```
AuthorResolverからは、Dataloaderのloadという関数を呼び出します。

```typescript:author.resolver.ts
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Author } from './author.model';
import { Book } from '../book/book.model';
import { BookDataloader } from '../book/book.dataloader';

@Resolver(() => Author)
export class AuthorResolver {
  constructor(private bookDataloader: BookDataloader) {}

  @ResolveField('books', () => [Book])
  public async getBooks(@Parent() author: Author): Promise<Book[]> {
    console.log('-------------getBooks START-------------');
    console.log('------> load');
    const books = await this.bookDataloader.load(author.id.toString());
    console.log('-------------getBooks END-------------');
    return books.map((book) => new Book(book));
  }
}
```
Queryが実行されたときの流れを簡単に記述すると、
<details><summary>Query</summary>

```graphql:query.gql
  query {
           authors {
            id
            name
            books {
              id
              title
            }
          }
        }
```
</details>

①Queryノードに"authors"ノードが含まれるので、対応するAuthorsResolverのgetAuthors関数が実行される（ココまで同じ）
②さらにAuthorノード内に"books"ノードが含まれるので、対応するgetBooks関数が実行され、dataloaderにauthor.idがloadされる
③親ノードであるAuthorのauthor.idが全てloadされると、BookDataloaderのbatchLoad関数が実行され、ここで初めて /books/findByAuthorIdsをコールする

という流れになります。

ちなみに、このときの実行時ログは次のようになります。
```
-------------getAuthors START-------------
GET http://localhost:8080/authors (16ms)
Response from Backend API: [{"id":1,"name":"Samuel Beckett","books":null},{"id":2,"name":"Anton Pavlovich Chekhov","books":null},{"id":3,"name":"William Shakespeare","books":null}]
-------------getAuthors END-------------
-------------getBooks START-------------
------> load
-------------getBooks START-------------
------> load
-------------getBooks START-------------
------> load
-------------BookDataloader batchLoad-------------
GET http://localhost:8080/books/findByAuthorIds?authorIds=2%2C3%2C1 (7ms)
Response from Backend API: [{"id":"978-0802144423","title":"Waiting for Godot","authorId":1},{"id":"978-0571244577","title":"Happy Days","authorId":1},{"id":"978-1559365512","title":"The Cherry Orchard","authorId":2}]
-------------getBooks END-------------
-------------getBooks END-------------
-------------getBooks END-------------
```
想定通り、HTTPリクエストが2回だけ実行されています。

また、Queryに"books"ノードが含まれない場合は、①の処理のみで終了するため、
```
-------------getAuthors START-------------
GET http://localhost:8080/authors (11ms)
Response from Backend API: [{"id":1,"name":"Samuel Beckett","books":null},{"id":2,"name":"Anton Pavlovich Chekhov","books":null},{"id":3,"name":"William Shakespeare","books":null}]
-------------getAuthors END-------------
```
このように/authorsのみが呼ばれます。

### 補足）親ノード：子ノードが１：多の場合
DataloaderのbatchLoad関数では、以下の制約を守る必要があります。[^3]
- 値の配列はキーの配列と同じ長さでなければならない
- 値の配列の各インデックスは、キーの配列の同じインデックスに対応していなければならない

今回のケースで言うと、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254418/3036490f-e992-d679-a05f-6ce7f4f937c1.png)

authorId=1には2冊の本が紐づき、authorId=2には1冊の本、authorId=3には0冊の本が紐づいているわけですが、このマッピングをDataloaderに教えてあげる必要があります。

この処理が、上述のgroupByAuthorId関数にあたります。

```typescript:book.dataloader.ts
  /**
   *
   * @param keys 親ノードAuthorのauthorIdの配列
   * @param books バックエンドAPIから取得したBookの配列
   * @returns key: authorId, value: Book[]のMap
   */
  private groupByAuthorId(
    keys: string[],
    books: Book[],
  ): Record<string, Book[]> {
    return Object.fromEntries(
      keys.map((key) => [
        key,
        books.filter((book) => key === book.authorId.toString()),
      ]),
    );
  }
}
```


## 結論
今回はGraphQLにおけるN+1問題の解決策として、
- 解決策1: JOIN FETCHする
- 解決策2: 幅優先(breadth-first)でフェッチする

という2つの方法を紹介しました。
特に後者のDataloaderを用いる方法は、「1つのノードに1つの対応するリゾルバを割り当てる」という設計指針にうまく落とし込めるものであり、より使いやすいように感じます。

ただし、あくまでパフォーマンスを追求する意味では、前者を採用する方がよりよいケースも存在します。具体的には、「業務上ほとんどの場合において、ネストされたオブジェクトも一緒に取得する」といったケースです。この場合は、1+1+......というクエリが発生するDataloaderを使うよりも、JOIN FETCHするようなクエリを1回だけ発行する方がより合理的である可能性もあります。

以上、「GraphQLにおけるN+1問題の解決策」でした。

具体的な実装例を下記にまとめたので、実際にコードを書いて確かめたい方はご参照ください。

## 実装例①：N+1問題が発生するプログラム
<details><summary>author.module.ts</summary>

```typescript:author.module.ts
import { Module } from '@nestjs/common';
import { AuthorsResolver } from './authors.resolver';
import { AuthorResolver } from './author.resolver';
import { AuthorService } from './author.service';
import { BookModule } from '../book/book.module';
import { BookService } from '../book/book.service';

@Module({
  imports: [BookModule],
  providers: [AuthorsResolver, AuthorResolver, AuthorService, BookService],
})
export class AuthorModule {}
```
</details>

<details><summary>authors.resolver.ts</summary>

```typescript:authors.resolver.ts
import { Query, Resolver } from '@nestjs/graphql';
import { Author } from './author.model';
import { AuthorService } from './author.service';

@Resolver(() => [Author])
export class AuthorsResolver {
  constructor(private authorService: AuthorService) {}

  @Query(() => [Author], { name: 'authors', description: '著者一覧取得' })
  public async getAuthors(): Promise<Author[]> {
    console.log('-------------getAuthors START-------------');

    const authors = await this.authorService.findAll();

    console.log('-------------getAuthors END-------------');
    return authors.map((author) => new Author(author));
  }
}
```
</details>

<details><summary>author.service.ts</summary>

```typescript:author.service.ts
import { Injectable } from '@nestjs/common';
import { RESTDataSource } from '@apollo/datasource-rest';
import { Author } from './author.backend.model';

@Injectable()
export class AuthorService extends RESTDataSource {
  constructor() {
    super();
  }

  public async findAll(): Promise<Array<Author>> {
    const data = await this.get(`http://localhost:8080/authors`);
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }
}
```
</details>

<details><summary>author.resolver.ts</summary>

```typescript:author.resolver.ts
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Author } from './author.model';
import { Book } from '../book/book.model';
import { BookService } from '../book/book.service';

@Resolver(() => Author)
export class AuthorResolver {
  constructor(private bookService: BookService) {}

  @ResolveField('books', () => [Book])
  public async getBooks(@Parent() author: Author): Promise<Book[]> {
    console.log('-------------getBooks START-------------');

    const books = await this.bookService.findByAuthorId(author.id);

    console.log('-------------getBooks END-------------');
    return books.map((book) => new Book(book));
  }
}
```
</details>

<details><summary>book.service.ts</summary>

```typescript:book.service.ts
import { Injectable } from '@nestjs/common';
import { RESTDataSource } from '@apollo/datasource-rest';
import { Book } from './book.backend.model';

@Injectable()
export class BookService extends RESTDataSource {
  constructor() {
    super();
  }

  public async findByAuthorId(authorId: number): Promise<Array<Book>> {
    const data = await this.get(
      `http://localhost:8080/authors/${authorId}/books`,
    );
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }
}
```
</details>

<details><summary>author.model.ts</summary>

```typescript:author.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Author as AuthorFromBackend } from './author.backend.model';

@ObjectType()
export class Author {
  @Field(() => Int, { description: '著者ID' })
  id!: number;

  @Field(() => String, { description: '著者名' })
  name!: string;

  constructor(author: AuthorFromBackend) {
    this.id = author.id;
    this.name = author.name;
  }
}
``` 
</details>

<details><summary>author.backend.model.ts</summary>

```typescript:author.backend.model.ts
export class Author {
  id!: number;
  name!: string;
}
```
</details>

<details><summary>book.model.ts</summary>

```typescript:book.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Book as BookFromBackend } from './book.backend.model';

@ObjectType()
export class Book {
  @Field(() => String, { description: 'ISBN' })
  id!: string;

  @Field(() => String, { description: 'タイトル' })
  title!: string;

  @Field(() => Int, { description: '著者ID' })
  authorId!: number;

  constructor(book: BookFromBackend) {
    this.id = book.id;
    this.title = book.title;
    this.authorId = book.authorId;
  }
}
``` 
</details>

<details><summary>book.backend.model.ts</summary>

```typescript:book.backend.model.ts
export class Book {
  id!: string;
  title!: string;
  authorId!: number;
}
``` 
</details>

## 実装例②：JOIN FETCHするプログラム
<details open><summary>authors.resolver.ts</summary>

```typescript:authors.resolver.ts
import { Info, Query, Resolver } from '@nestjs/graphql';
import { Author } from './author.model';
import { AuthorService } from './author.service';

@Resolver(() => [Author])
export class AuthorsResolver {
  constructor(private authorService: AuthorService) {}

  @Query(() => [Author], { name: 'authors', description: '著者一覧取得' })
  public async getAuthors(@Info() info: any): Promise<Author[]> {
    console.log('-------------getAuthors START-------------');

    if (this.doesPathExist(info, 'books')) {
      const authors = await this.authorService.findAllWithBooks();

      console.log('-------------getAuthors END-------------');
      return authors.map((author) => new Author(author));
    } else {
      const authors = await this.authorService.findAll();

      console.log('-------------getAuthors END-------------');
      return authors.map((author) => new Author(author));
    }
  }

  /**
   *
   * @param info GraphQLのinfoオブジェクト
   * @param path 判定したいノード
   * @returns Queryにpathが含まれる場合はtrue, それ以外はfalse
   */
  private doesPathExist(info: any, path: string): boolean {
    for (const selection of info.fieldNodes[0].selectionSet.selections) {
      if (selection.name.value == path) {
        return true;
      }
    }
    return false;
  }
}

```
</details>

<details open><summary>author.service.ts</summary>

```typescript:author.service.ts
import { Injectable } from '@nestjs/common';
import { RESTDataSource } from '@apollo/datasource-rest';
import { Author } from './author.backend.model';

@Injectable()
export class AuthorService extends RESTDataSource {
  constructor() {
    super();
  }

  public async findAll(): Promise<Array<Author>> {
    const data = await this.get(`http://localhost:8080/authors`);
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }

  public async findAllWithBooks(): Promise<Array<Author>> {
    const data = await this.get(`http://localhost:8080/authorsWithBooks`);
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }
}
```
</details>

## 実装例③：Dataloaderを利用したプログラム
<details><summary>author.module.ts</summary>

```typescript:author.module.ts
import { Module } from '@nestjs/common';
import { AuthorsResolver } from './authors.resolver';
import { AuthorResolver } from './author.resolver';
import { AuthorService } from './author.service';
import { BookModule } from '../book/book.module';
import { BookService } from '../book/book.service';
import { BookDataloader } from '../book/book.dataloader';

@Module({
  imports: [BookModule],
  providers: [
    AuthorsResolver,
    AuthorResolver,
    AuthorService,
    BookService,
    BookDataloader,
  ],
})
export class AuthorModule {}

```
</details>

<details open><summary>authors.resolver.ts</summary>

```typescript:authors.resolver.ts
import { Query, Resolver } from '@nestjs/graphql';
import { Author } from './author.model';
import { AuthorService } from './author.service';

@Resolver(() => [Author])
export class AuthorsResolver {
  constructor(private authorService: AuthorService) {}

  @Query(() => [Author], { name: 'authors', description: '著者一覧取得' })
  public async getAuthors(): Promise<Author[]> {
    console.log('-------------getAuthors START-------------');

    const authors = await this.authorService.findAll();

    console.log('-------------getAuthors END-------------');
    return authors.map((author) => new Author(author));
  }
}
```
</details>

<details open><summary>book.dataloader.ts</summary>

```typescript:book.dataloader.ts
import { BaseDataloader } from '@/graphql/base-dataloader';
import { Injectable, Scope } from '@nestjs/common';
import { Book } from './book.backend.model';
import { BookService } from './book.service';

@Injectable({ scope: Scope.REQUEST })
export class BookDataloader extends BaseDataloader<string, Book[]> {
  constructor(private readonly bookService: BookService) {
    super();
  }

  protected async batchLoad(keys: string[]): Promise<Array<Book[]>> {
    console.log('-------------BookDataloader batchLoad-------------');

    const books = await this.bookService.findByAuthorIds(keys);

    // authorId単位でbooksを仕分ける
    const groupedByAuthorId = this.groupByAuthorId(keys, books);

    return keys.map((key) => groupedByAuthorId[key]);
  }

  /**
   *
   * @param keys 親ノードAuthorのauthorIdの配列
   * @param books バックエンドAPIから取得したBookの配列
   * @returns key: authorId, value: Book[]のMap
   */
  private groupByAuthorId(
    keys: string[],
    books: Book[],
  ): Record<string, Book[]> {
    return Object.fromEntries(
      keys.map((key) => [
        key,
        books.filter((book) => key === book.authorId.toString()),
      ]),
    );
  }
}
```
</details>

<details><summary>base-dataloader.ts</summary>

```typescript:base-dataloader.ts
import * as DataLoader from 'dataloader';

export abstract class BaseDataloader<K, V> extends Object {
  protected dataloader: DataLoader<K, V> = new DataLoader<K, V>(
    this.batchLoad.bind(this),
  );

  public clear(key: K): DataLoader<K, V> {
    return this.dataloader.clear(key);
  }

  public clearAll(): DataLoader<K, V> {
    return this.dataloader.clearAll();
  }

  public async load(key: K): Promise<V> {
    return this.dataloader.load(key);
  }

  public async loadMany(keys: K[]): Promise<(V | Error)[]> {
    return this.dataloader.loadMany(keys);
  }

  public prime(key: K, value: V): DataLoader<K, V> {
    return this.dataloader.prime(key, value);
  }

  protected abstract batchLoad(keys: K[]): Promise<(V | Error)[]>;
}
```
</details>

<details open><summary>book.service.ts</summary>

```typescript:book.service.ts
import { Injectable } from '@nestjs/common';
import { RESTDataSource } from '@apollo/datasource-rest';
import { Book } from './book.backend.model';

@Injectable()
export class BookService extends RESTDataSource {
  constructor() {
    super();
  }

  public async findByAuthorId(authorId: number): Promise<Array<Book>> {
    const data = await this.get(
      `http://localhost:8080/authors/${authorId}/books`,
    );
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }

  public async findByAuthorIds(authorIds: string[]): Promise<Array<Book>> {
    const idsString = authorIds.join(',');
    const data = await this.get(`http://localhost:8080/books/findByAuthorIds`, {
      params: {
        authorIds: idsString,
      },
    });
    console.log('Response from Backend API: ' + JSON.stringify(data));
    return data;
  }
}
```
</details>



[^1]: infoオブジェクトの詳細な型定義は[こちら](https://github.com/graphql/graphql-js/blob/bacd412770f4c21f40d403d605420e9f4fd9ed2f/src/type/definition.js#L584-L595)
[^2]: GraphQLの思想を十分に調べたわけではないので、あくまで個人の見解です。
[^3]: https://github.com/graphql/dataloader#batch-function