バックエンドにあるREST APIからデータを取得して、GraphQLのスキーマに載せて返すようなBFF(Backend for Frontend)を設計しています。
その過程で、GraphQLでN+1問題を回避するにはどうしたらいいか?という課題に直面したので、今回はその解決策をまとめてみます。
N+1問題とは何か
まず、N+1問題とは何かという話から始めます。
例えば、データベース上にAuthor(著者)テーブルとBook(本)テーブルが存在し、関連は次のようであるとします。
今回は「著者の一覧を、各著者に紐づく本の情報とともに取得できるようにしてほしい」というクライアントからの要件を受けて、次のようなGraphQLのスキーマを定義しました。
type Query {
"""著者一覧取得"""
authors: [Author!]!
}
type Author {
books: [Book!]!
"""著者ID"""
id: Int!
"""著者名"""
name: String!
}
type Book {
"""著者ID"""
authorId: Int!
"""ISBN"""
id: String!
"""タイトル"""
title: String!
}
図で描くとこんな感じです。
そして、Queryノード内で"authors"ノードが要求されたときに動くリゾルバと、Authorノード内で"books"ノードが要求されたときに動くリゾルバを、かくかくしかじかと定義しました。(具体的な実装はこちら)
さて、一通りのプログラムが完成したので、いよいよ動作確認です。GraphQLのエンドポイント(/graphql)に向けて、次のようなQueryを投げてみます。
query {
authors {
id
name
books {
id
title
}
}
}
すると、次のようにばっちりデータを取得することができました。めでたしめでたし。
{
"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で考えると、
SELECT * FROM Author;
SELECT * FROM Book WHERE author_id = 2;
SELECT * FROM Book WHERE author_id = 3;
SELECT * FROM Book WHERE author_id = 1;
のようなクエリが発行されている状態です。
これを回避するために、
SELECT * FROM Author a LEFT JOIN Book b ON a.id = b.author_id;
というSQLを発行するようなREST APIのエンドポイント(/authorsWithBooks)を作成し、BFF(GraphQL)からはこのエンドポイントを呼び出すようにします。
具体的な実装は次のようになります。
まず、REST APIを呼び出す処理をまとめたクラスに、/authorsWithBooksを呼び出すための関数を追加します。
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が要求されたときに動くリゾルバで、この関数が呼ばれるように修正します。
@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ノード内のリゾルバを削除します。
@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を実行してみます。すると、今度も同じ結果を得ることができるはずです。
Query
query {
authors {
id
name
books {
id
title
}
}
}
レスポンス結果
{
"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/authorsWithBooks (7ms)
はい、今度は1回のHTTPリクエストで処理が完結しています。
info.fieldNodesの利用
しかし、このコーディングには1つの問題点があります。
例えば、Queryの内容が次のように、Authorに直接紐づくノードのみを要求し、booksノードを要求しなかった場合、処理はどのようになるでしょうか。
query {
authors {
id
name
}
}
答えはもちろん、"authors"に紐づくリゾルバが動いた結果、authorService.findAllWithBooksが呼ばれるため、
GET http://localhost:8080/authorsWithBooks (6ms)
このエンドポイントにHTTPリクエストを送信することになります。
しかしこれでは、バックエンドのREST API側で本来やる必要のないJOINが発生していることになり、パフォーマンスを低下させてしまいます。
この問題を解消するためには、リクエストされたQuery内で具体的にどのノードが要求されたかを見て、どのバックエンドAPIを呼び出すべきかを判断する必要があります。
各リゾルバ内で、リクエストされたQueryのツリー構造を知るためには、GraphQLのinfoオブジェクト 1を利用します。
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問題が発生する原因をグラフを用いて考えます。
上図のように、"authors"に紐づくN件の"Author"を取得し、さらに"Author"の子ノードである"books"を取得し......という具合に、深さ優先(depth-first)の考え方で処理をしていくと、どうしても親ノードの数だけクエリが発生してしまいます。
しかし、とある賢人は考えました。
幅優先(breadth-first)でレイヤーごとにデータ取得をするようにすれば、「クエリの数=ネストの数」に抑えることができるのではないか?(つまり、N+1問題を1+1+......にできる!)
図にするとこんな感じです。ポイントは、今まで
SELECT * FROM Book WHERE author_id = :authorId;
だったクエリが、
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に
SELECT * FROM Book WHERE author_id IN (:authorIds);
に対応するエンドポイント(/books/findByAuthorIds)を作成します。
そして、REST APIを呼び出す処理をまとめたクラスに、/books/findByAuthorIdsを呼び出すための関数を追加します。
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を噛ませます。
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)[]>;
}
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という関数を呼び出します。
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が実行されたときの流れを簡単に記述すると、
Query
query {
authors {
id
name
books {
id
title
}
}
}
①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のみが呼ばれます。
補足)親ノード:子ノードが1:多の場合
DataloaderのbatchLoad関数では、以下の制約を守る必要があります。3
- 値の配列はキーの配列と同じ長さでなければならない
- 値の配列の各インデックスは、キーの配列の同じインデックスに対応していなければならない
今回のケースで言うと、
authorId=1には2冊の本が紐づき、authorId=2には1冊の本、authorId=3には0冊の本が紐づいているわけですが、このマッピングをDataloaderに教えてあげる必要があります。
この処理が、上述のgroupByAuthorId関数にあたります。
/**
*
* @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問題が発生するプログラム
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 {}
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));
}
}
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;
}
}
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));
}
}
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;
}
}
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;
}
}
author.backend.model.ts
export class Author {
id!: number;
name!: string;
}
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;
}
}
book.backend.model.ts
export class Book {
id!: string;
title!: string;
authorId!: number;
}
実装例②:JOIN FETCHするプログラム
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;
}
}
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;
}
}
実装例③:Dataloaderを利用したプログラム
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 {}
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));
}
}
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()),
]),
);
}
}
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)[]>;
}
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;
}
}
-
GraphQLの思想を十分に調べたわけではないので、あくまで個人の見解です。 ↩