この記事では、既存の任意の GraphQL APIを使用し、それを自身のサーバーを通じて公開する方法を理解したいと考えています。このセットアップでは、サーバーは受け取ったGraphQLクエリとミューテーションを、基盤となるGraphQL APIに転送するだけです。これらの操作の転送を担当するコンポーネントは、リモート(実行可能)スキーマと呼ばれます。
リモートスキーマは、GraphQLコミュニティにおける真新しいトピックであるスキーマステッチングと呼ばれるツールとテクニックのセットの基礎となります。以下の記事では、スキーマステッチングのさまざまなアプローチについてさらに詳しく議論します。
要約: GraphQLスキーマ
前回の記事では、GraphQLスキーマの基本的なメカニズムと内部動作についてすでに説明しました。簡単に復習しましょう!
始める前に、GraphQLスキーマという用語はいくつかの意味を持つ可能性があるため、その意味を明確にすることが重要です。この記事の文脈では、主にNode.jsで書かれたGraphQLサーバーの基礎として使用される、GraphQL.js参照実装によって提供されるGraphQLSchema
クラスのインスタンスを指す言葉として使用します。
スキーマは主に2つのコンポーネントで構成されています
- スキーマ定義: この部分は通常、GraphQLのスキーマ定義言語(SDL)で記述され、APIの機能を抽象的に記述します。そのため、まだ実際の実装はありません。要するに、スキーマ定義はサーバーが受け入れる操作(クエリ、ミューテーション、サブスクリプション)の種類を指定します。スキーマ定義が有効であるためには、
Query
型を含んでいる必要があり、オプションでMutation
型やSubscription
型を含めることができます。(コード内でスキーマ定義を参照する場合、対応する変数は通常typeDefs
と呼ばれます。) - リゾルバ: ここでスキーマ定義が実体化し、実際の動作を獲得します。リゾルバは、スキーマ定義によって指定されたAPIを実装します。(詳細については、前回の記事を参照してください。)
スキーマがスキーマ定義とリゾルバ関数の両方を持つ場合、それを実行可能スキーマとも呼びます。
GraphQLSchema
のインスタンスが必ずしも実行可能であるとは限りません。スキーマ定義のみを含み、リゾルバがアタッチされていない場合もあります。
以下に、graphql-tools
のmakeExecutableSchema
関数を使用した簡単な例を示します。
typeDefs
には、必須のQuery
型とシンプルなUser
型を含むスキーマ定義が含まれています。resolvers
は、Query
型で定義されたuser
フィールドの実装を含むオブジェクトです。
makeExecutableSchema
は、スキーマ定義内のSDL型からのフィールドを、resolvers
オブジェクトで定義された対応する関数にマッピングします。これによりGraphQLSchema
のインスタンスが返され、例えばGraphQL.jsのgraphql
関数を使用して実際のGraphQLクエリを実行できるようになります。
graphql
関数はGraphQLSchema
のインスタンスに対してクエリを実行できるため、GraphQL(実行)エンジンとも呼ばれます。
GraphQL実行エンジンは、実行可能スキーマとクエリ(またはミューテーション)が与えられた場合に、有効なレスポンスを生成するプログラム(または関数)です。したがって、その主な役割は、実行可能スキーマ内のリゾルバ関数の呼び出しを調整し、GraphQL仕様に従って、レスポンスデータを適切にパッケージ化することです。
この知識をもって、既存のGraphQL APIに基づいてGraphQLSchema
の実行可能インスタンスを作成する方法について掘り下げていきましょう。
GraphQL APIのイントロスペクション
GraphQL APIの便利な特性の1つは、イントロスペクションを可能にすることです。これは、いわゆるイントロスペクションクエリを送信することで、任意のGraphQL APIのスキーマ定義を抽出できることを意味します。
上記の例を考慮すると、次のクエリを使用して、スキーマからすべての型とそのフィールドを抽出できます。
これにより、以下のJSONデータが返されます。
ご覧の通り、このJSONオブジェクトの情報は、上記のSDLベースのスキーマ定義と同等です(実際には、フィールドの引数を要求していないため100%同等ではありませんが、上記のイントロスペクションクエリを拡張してこれらを含めることもできます)。
リモートスキーマの作成
既存のGraphQL APIのスキーマをイントロスペクトする機能があれば、既存のスキーマ定義と同一の新しいGraphQLSchema
インスタンスを簡単に作成できます。これこそが、graphql-tools
のmakeRemoteExecutableSchema
の考え方です。
makeRemoteExecutableSchema
は2つの引数を受け取ります。
- スキーマ定義(上記で見たイントロスペクションクエリを使用して取得できます)。スキーマ定義は、ランタイムにイントロスペクションクエリを送信する(大きなパフォーマンスオーバーヘッドにつながる)のではなく、開発時にダウンロードし、
.graphql
ファイルとしてサーバーにアップロードすることがベストプラクティスとされています。 - プロキシされるGraphQL APIに接続されたLink。本質的に、このLinkは既存のGraphQL APIにクエリやミューテーションを転送できるコンポーネントであり、その(HTTP)エンドポイントを知っている必要があります。
makeRemoteExecutableSchema
の実装はここからは非常に簡単です。スキーマ定義は新しいスキーマの基盤として使用されます。しかし、リゾルバはどうでしょうか、どこから来るのでしょうか?
もちろん、スキーマ定義をダウンロードするのと同じ方法でリゾルバをダウンロードすることはできません。リゾルバのイントロスペクションクエリは存在しません。しかし、言及したLinkコンポーネントを使用して、受信したクエリやミューテーションを基盤となるGraphQL APIに単に転送する新しいリゾルバを作成することは可能です。
うんざりする話はこれくらいにして、コードを見てみましょう!以下に、User
という型のGraphcool CRUD APIに基づき、リモートスキーマを作成し、それを専用サーバー(graphql-yoga
を使用)を通じて公開する例を示します。
このコードの動作例はこちらで見つけることができます。
参考までに、User型に対するCRUD APIはこれといくらか似ています(完全なバージョンはこちらで見つけることができます)。
リモートスキーマの内部構造
上記の例のdatabaseServiceSchemaDefinition
とdatabaseServiceExecutableSchema
が内部でどのように見えるか調べてみましょう。
GraphQLスキーマの調査
まず注目すべきは、両方ともGraphQLSchema
のインスタンスであるということです。ただし、databaseServiceSchemaDefinition
にはスキーマ定義のみが含まれているのに対し、databaseServiceExecutableSchema
は実際に実行可能なスキーマであり、その型のフィールドにリゾルバ関数がアタッチされています。
Chromeデバッガを使用すると、databaseServiceSchemaDefinition
が次のようなJavaScriptオブジェクトであることがわかります。
GraphQLSchemaの実行不可能なインスタンス
青い四角は、そのプロパティを持つQuery
型を示しています。予想通り、allUsers
というフィールド(その他にも)があります。しかし、このスキーマインスタンスにはQuery
のフィールドにリゾルバがアタッチされていないため、実行可能ではありません。
では、databaseServiceExecutableSchema
も見てみましょう。
実行可能スキーマ = スキーマ定義 + リゾルバ
このスクリーンショットは先ほど見たものと非常によく似ていますが、allUsers
フィールドにresolve
関数がアタッチされています。(これはQuery
型にある他のフィールド(User
、node
、user
、_allUsersMeta
)についても同様ですが、スクリーンショットには表示されていません。)
さらに一歩踏み込んで、resolve
関数の実装を見てみましょう(このコードはmakeRemoteExecutableSchema
によって動的に生成されたものです)。
私たちにとって興味深いのは12行目から16行目です。fetcher
という関数が、query
、variables
、context
の3つの引数で呼び出されています。fetcher
は、以前に提供したLinkに基づいて生成されたもので、基本的に特定の(Linkを作成するために使用された)エンドポイントにGraphQL操作を送信できる関数であり、ここでまさにそれを実行しています。13行目でクエリの値として渡される実際のGraphQLドキュメントが、リゾルバに渡されるinfo引数(10行目を参照)から来ていることに注目してください。info
にはクエリのAST表現が含まれています。
非ルートリゾルバはネットワーク呼び出しを行わない
上記でallUsers
ルートフィールドのリゾルバ関数を探索したのと同じように、User
型のフィールドのリゾルバがどのように見えるかも調べることができます。そのため、databaseServiceExecutableSchema
の_typeMaps
プロパティに移動し、そこにフィールドを持つUser
型を見つける必要があります。
User型には2つのフィールドがあります: idとname(両方にリゾルバ関数がアタッチされています)
両方のフィールド(id
とname
)にはresolve
関数がアタッチされており、ここにmakeRemoteExecutableSchema
によって生成されたその実装があります(両方のフィールドで同一であることに注意してください)。
興味深いことに、今回は生成されたリゾルバはfetcher
関数を使用していません。実際には、ネットワークへの呼び出しはまったく行いません。返される結果は、関数に渡されるparent
引数(10行目)から単純に取得されます。
リモートスキーマにおけるリゾルバデータのトレース
リモート実行可能スキーマのリゾルバに関するトレースデータもこの発見を裏付けています。以下のスクリーンショットでは、以前のスキーマ定義をArticle
型とComment
型(それぞれexistingUser
にも接続されています)で拡張し、より深くネストされたクエリを送信できるようにしました。
GraphQL Playgroundsは、リゾルバのトレースデータをすぐに表示する機能をサポートしています(右下)
トレースデータから、ルートリゾルバ(allUsers
フィールド用)だけが顕著な時間(167ミリ秒)を要していることが非常によく分かります。非ルートフィールドのデータを返す役割を担う残りのすべてのリゾルバは、実行にわずか数マイクロ秒しかかかっていません。これは、ルートリゾルバがfetcher
を使用して受信したクエリを転送する一方、すべての非ルートリゾルバは受信したparent
引数に基づいてデータを単純に返すという、以前に行った観察で説明できます。
リゾルバの戦略
スキーマ定義のリゾルバ関数を実装する際には、いくつかの方法があります。
標準パターン: 型レベルのリゾルビング
次のスキーマ定義を考えてみましょう。
Query
型に基づいて、次のクエリをAPIに送信することが可能です。
対応するリゾルバは通常どのように実装されるでしょうか?この標準的なアプローチは以下のようになります(このコードでfetch
で始まる関数はデータベースからリソースをロードしていると仮定します)。
このアプローチでは、型レベルで解決しています。これは、特定のクエリ(例: 特定のArticle
)に対する実際のオブジェクトが、Article
型のいずれのリゾルバも呼び出される前にフェッチされることを意味します。
上記のクエリに対するリゾルバの呼び出しを考えてみましょう。
Query.user
リゾルバが呼び出され、データベースから特定のUser
オブジェクトをロードします。クエリで要求されていないにもかかわらず、id
やname
を含むUser
オブジェクトのすべてのスカラフィールドをロードすることに注意してください。articles
についてはまだ何もロードしていませんが、これは次のステップで起こることです。- 次に、
User.articles
リゾルバが呼び出されます。入力引数parent
が前のリゾルバからの戻り値であることに注目してください。つまり、それは完全なUser
オブジェクトであり、リゾルバがUser
のid
にアクセスしてそのArticle
オブジェクトをロードすることを可能にします。
この例を理解するのが難しい場合は、GraphQLスキーマに関する前回の記事を必ず読んでください。
リモート実行可能スキーマはマルチレベルリゾルバアプローチを使用する
では、リモートスキーマの例とそのリゾルバについてもう一度考えてみましょう。リモート実行可能スキーマを使用してクエリを実行する場合、データソースはルートリゾルバ(上記のスクリーンショットでfetcher
を見つけた場所)で一度だけヒットすることがわかりました。他のすべてのリゾルバは、入力されたparent
引数(最初のリゾルバ呼び出しの結果のサブパート)に基づいて正規の結果を返します。
しかし、それはどのように機能するのでしょうか?ルートリゾルバが単一のリゾルバですべて必要なデータをフェッチしているように見えますが、これは非常に非効率ではないでしょうか?確かに、常にすべてのオブジェクトフィールドを含めてすべてのリレーショナルデータをロードするならば、それは非常に非効率でしょう。では、受信したクエリで指定されたデータのみをロードするにはどうすればよいでしょうか?
これが、リモート実行可能スキーマのルートリゾルバが、クエリ情報を含む利用可能なinfo引数を利用する理由です。実際のリゾルバの選択セットを見ることで、リゾルバはオブジェクトのすべてのフィールドをロードする必要はなく、必要なフィールドのみをロードします。この「トリック」により、単一のリゾルバですべてのデータをロードすることが依然として効率的になります。
まとめ
この記事では、graphql-tools
のmakeRemoteExecutableSchema
を使用して、既存のGraphQL APIのプロキシを作成する方法を学びました。このプロキシはリモート実行可能スキーマと呼ばれ、自身のサーバー上で動作します。これは、受け取ったクエリをすべて基盤となるGraphQL APIに転送するだけです。
また、このリモート実行可能スキーマがマルチレベルリゾルバを使用して実装されていることも確認しました。ここでは、ネストされたデータが型レベルで複数回ではなく、最初のリゾルバによって1回だけフェッチされます。
リモートスキーマについてはまだ多くの発見があります: スキーマステッチングとどのように関連するのか?GraphQLサブスクリプションとはどのように連携するのか?私のcontext
オブジェクトはどうなるのか?次に何を学びたいかコメントで教えてください!👋
次回の投稿をお見逃しなく!
Prismaニュースレターに登録する