GraphQLサーバー
GraphQLは、クライアント側のメリットで知られています。GraphQLサーバーは、最小限のデータベース検索とAPI呼び出し回数で、適切なデータ量を取得するという重要な仕事を担っています。
GraphQL要求は、selection setやfieldなどの操作定義を含むGraphQLサービスのスキーマや、GraphQLドキュメントを使って、サーバーで executed
します。
サーバーは以下の手順を実行します。
- ドキュメントのパース
- 操作が複数ある場合は、実行する操作を指定
- 要求を検証し、失敗した場合はエラーを返す
- 操作(query / mutation / subscription)の実行
GraphQLサーバーの記述方法には様々なアプローチがあります。GraphQLコミュニティで最も一般的に使われているアプローチを紹介します。
Resolverアプローチ
GraphQLサーバーのコードを記述する上で最も一般的なのアプローチは、スキーマを定義して、resolversで様々な操作やfieldを記述する方法です。
コンテキストベースの特定fieldの処理手順を指示する機能をresolversが担っていると考えてください。
resolverにおける基本的なsignatureを以下に示します。
resolverFunc(data, args, context, info)
data
- 親から予め取得していたデータ。args
- 引数のkey-valueのペア、オプション。context
- 要求ごとのステート情報、通常は認証ロジックに使用info
- トラバーサルに使うcontextを選択するためのメタデータ
今では、このresolver関数はGraphQL queryのあらゆるフィールドにおいて実行されるようになりました。
N+1によるパフォーマンスへの影響
例えば、articlesとauthorのリストを取得する必要があるとします。シンプルなREST APIをナイーブなコードで作成すると次のようになります。
fetchData: async () => ORM.getAuthors().getArticles();
ここではデータベースに対してquery(SQL)が2件あります。一つはauthorのリストを取得し、もう一つはauthorごとのarticlesのリストを取得します。
このコードにGraphQLを使ってみましょう。
GraphQL queryを使うと次のようになります。
query {author {idnamearticles {idtitlecontent}}}
resolverを使うと次のようになります。
resolvers = {Query: {author: async () => {return ORM.getAllAuthors()}},Author: {articles: async (authorObj, args) => {return ORM.getArticlesBy(authorObj.id)}},}
このコードが実際どのように動作するか考えてみましょう。3件それぞれのauthorに2件のarticlesがリンクされているとします。
最初のresolverによって author
が呼び出され、すべてのauthor(この場合3件)が返ります。リレーショナルクエリの articles
では、resolverの articles
がauthorごとに一回ずつ呼び出されます。このナイーブなアプローチの場合、データベースへのヒットは計4件(authorで1件、articlesで3件)となります。
このアプローチだと、パフォーマンスに明らかな影響を及ぼすことが分かります。
Dataloader
Dataloaderは、アプリケーションのデータ取得レイヤーの一部として使用するユーティリティです。N+1問題の解決策には、すべてのresolverが値をロードしてからcoalesceを実行して、要求キーを使ってバッチ関数を呼び出すという方法があります。
コンパイラアプローチ
resolverをバッチ処理することで、N+1によるパフォーマンスへの影響は大幅に解消されます。これにより、データベースのマルチヒットが減少します。バッチ処理をしていても、クエリの深さによってはデータベースのマルチヒットが発生します。
コンパイラアプローチにより、任意の深さのGraphQL queryを1つのデータベースqueryにマップすることができます。データベースからのデータをGraphQL queryで処理する場合は、このアプローチの方がより高いパフォーマンスを発揮できます。
Hasuraのコンパイラアプローチを使って、GraphQL queryの実行性能を向上させる方法については、 こちら からご覧ください。
ハイブリッドアプローチ
様々なソースから集められたデータを扱う場合は、上記のアプローチを組み合わせて対応します。コンパイラアプローチは、データベース部分のqueryに対して有効であり、DataLoaderを使ったqueryのバッチ処理は、外部データソース / HTTP要求のバッチ処理に対して最適です。
ハイブリッドアプローチのアーキテクチャの場合、主要なCRUD操作はデータベースと接続したサーバーで行います。一方、他のソースから取得・ミューテーションしたフィールドの実行にはresolverアプローチを使います。
GraphQLサーバーのコードを新しく作成する場合は、フィールドごとのqueryをresolveする関数を記述するresolverアプローチを使用します。インスタントCRUDを使うためのデータベースをGraphQLにマップする場合は、コンパイラアプローチが適しています。
Hasuraのようなサーバーでは、データベース処理向けのインスタントCRUDが提供されており、カスタムされたビジネスロジックが他にあっても独自のresolverを記述できます。このため、一般的にはハイブリッドアプローチが推奨されています。
- Build apps and APIs 10x faster
- Built-in authorization and caching
- 8x more performant than hand-rolled APIs