本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはmangano-itoさんと中岡大樹さんで、テーマは「Perlで作るGraphQL API」です。
GraphQLはAPIのためのクエリ言語です。クエリの柔軟性やスキーマで構造を記述できるメリットがあり、GitHubをはじめとした多くのWebサービスのAPIに採用されています。
本稿で解説すること
本稿では、Perlで実践的なGraphQL APIを開発する手法を解説します。スキーマのフィールドに対応するデータを返す関数であるリゾルバを中心に解説します。リゾルバに関連して、N+1問題を解決するためのデータローダ、パフォーマンスを改善するためのキャッシュレイヤも実装します。
GraphQLのスキーマやクエリのパースには、graphql-perlが提供するGraphQL::SchemaやGraphQL::Executionを使います。また、本稿のコードは、執筆時点(2022年5月)の最新であるPerl 5.34.1とGraphQL 0.54で動作確認を行っています。本稿のサンプルコードは、本誌サポートサイトから入手できます。以降で省略しているコードはこちらを参照してください。
なお、本稿ではGraphQLの文法などについての解説は省きますので、ドキュメントや解説書などを参照してください。また、GraphQL APIの実装の前提としてPSGI(Perl Web Server Gateway Interface)サーバを用意しておく必要があります。
Perlで実装してみよう
それでは、PerlでGraphQL APIを実装してみましょう。本節では、クエリの実行に必要なリゾルバを実装します。
スキーマの設計
今回は、電子書籍サービスのAPIのためのスキーマを考えます。書籍を取得するクエリを考えてみましょう。書籍を表すBook
型と筆者を表すAuthor
型を作ります。
最小の実装
App::GraphQL
モジュールを作り、スキーマのパースおよびリクエストされたクエリの実行を担当させます。
ここでは説明を省きますが、サーバとApp::GraphQL
をつなぎ込み、リクエストからquery
、variables
、operationName
パラメータを渡し、得られた結果をJSONにエンコードしてリクエストからレスポンスを得られるようにしてください。
リゾルバの実装
さて、本項ではそれぞれの型に対して個別にリゾルバのモジュールを作成します。
個別のリゾルバを実装する
App::GraphQL::Resolver::Book
モジュールを用意して、type Book
に対してはApp::GraphQL::Resolver::Book
モジュールを、type Query
に対してはApp::GraphQL::Resolver::Query
モジュールを担当させましょう。
コードは省略しますが、同様にして、Query
型のbook
フィールドに対応するApp::GraphQL::Resolver::Query
モジュールにbook
サブルーチンを作ってください。それぞれのtype
に応じたリゾルバをモジュールとして実装する必要があります。
動的にリゾルバを委譲する
ライブラリのデフォルトのリゾルバにすべての処理を追加すると、見通しが悪くなります。そこでデフォルトの実装を置き換えて、先ほど作成したモジュールにディスパッチしてリゾルバを委譲していきます。
委譲を実装しました。簡単にするために、HashRef
型のプロパティまたは導入された個別のリゾルバモジュールへのディスパッチのみに絞っています。
先ほどexecute
サブルーチンの(1)でGraphQL::Execution::execute
を実行していましたが、実は7番目の引数はリゾルバを指定するもので、未指定ではデフォルトの実装が使われるため、作成したリゾルバ実装の参照を渡して上書きします。
こうして、自作のリゾルバ実装を使えました。
実際のデータ取得クエリの成功を確認する
次はBookを取得するクエリを実行してみましょう。
data.book.title
フィールドに対して値としてMy Book
が返ってきてリクエストに成功します。リゾルバ実装ができましたね。ここからさらに発展させて、実際にAPI構築ができます。
データローダによるN+1問題の解決
N+1問題とは、ループ中でデータソースに対する多数のクエリが発行される問題です。GraphQLではグラフをたどるクエリを書けるので、N+1問題の起こりやすさは想像に難くないでしょう。この問題を防ぐために、データローダと呼ばれるしくみがあります。
データローダでは、各リゾルバでのデータ取得を集約し、集約した各取得処理を束ねてバッチとして取得するしくみを実装します。
Promiseライブラリを使った遅延評価
各リゾルバではそれぞれの単一のデータの結果がないと処理を継続できません。この問題を解決するために、Promiseを導入します。
Promiseは、JavaScriptでよく知られた遅延評価を行う概念です。Promiseを使うことで、個々のリゾルバでデータが返ってきたかのように処理を継続でき、最終的に一括でデータ取得を行うことができます。
graphql-perlが要求するインタフェースに合うものとして、Promise::XS
ライブラリを使います。Promise::XS
を使うと、データローダは単一のキーのデータ取得処理を次のように書けます。
また、graphql-perl
がPromise
を処理できるように、Promise
のインタフェースを合わせて渡します。
リゾルバでのデータローダの使用
既存のQuery.book
フィールドのリゾルバについて、データローダを使うリファクタをしましょう。
まず、IDから対応するBook
型のデータを取得するため、複数のIDからバッチで取得する実装を用いるデータローダを定義します。
次に、リゾルバで今まで単一のIDから取得する実装を直接使っていたところを、データローダを経由して取得するように変更します。
結果として、発行されるSQLは以下となり、データローダ未使用の場合は複数回で取得していたものが、IN
句により1回で取得できました。
<続きの(2)はこちら。>