2017年12月6日

GraphQLリモートスキーマはどのように機能するのか?

GraphQLスキーマステッチングの理解(パート1)

How do GraphQL remote schemas work?

この記事では、既存の*あらゆる*GraphQL APIを利用し、それを自身のサーバーを通して公開する方法を理解することを目的としています。その構成において、私たちのサーバーは、受信したGraphQLクエリとmutationを基盤となるGraphQL APIに*転送*するだけです。これらの操作の転送を担当するコンポーネントは、*リモート(実行可能)スキーマ*と呼ばれます。

リモートスキーマは、GraphQLコミュニティで全く新しいトピックである*スキーマステッチング*と呼ばれるツールと技術の基盤です。今後の記事では、スキーマステッチングへのさまざまなアプローチについてより詳細に議論します。

復習:GraphQLスキーマ

以前の記事で、GraphQLスキーマの基本的な仕組みと内部動作について既に説明しました。簡単な復習をしましょう!

始める前に、*GraphQLスキーマ*という用語はいくつかの意味を持つ可能性があるため、明確にしておくことが重要です。この記事の文脈では、主にGraphQLSchemaクラスのインスタンスを指す用語として使用します。これはGraphQL.jsのリファレンス実装によって提供され、Node.jsで記述されたGraphQLサーバーの基盤として使用されます。

スキーマは、2つの主要なコンポーネントで構成されています。

  • スキーマ定義:この部分は通常、GraphQL スキーマ定義言語(SDL)で記述され、APIの機能を*抽象的*に記述するため、まだ実際の*実装*はありません。本質的に、スキーマ定義は、サーバーが受け入れる操作の種類(クエリ、mutation、subscription)を指定します。スキーマ定義が有効であるためには、Query型、およびオプションでMutation型やSubscription型を含む必要があることに注意してください。(コード内でスキーマ定義を参照する場合、対応する変数は通常typeDefsと呼ばれます。)
  • リゾルバー:ここでスキーマ定義が*生き生きとし*、実際的な*動作*を受け取ります。リゾルバーは、スキーマ定義によって指定されたAPIを*実装*します。(詳細については、前回の記事を参照してください。)

スキーマがスキーマ定義とリゾルバー関数を持っている場合、それを実行可能スキーマとも呼びます。GraphQLSchemaのインスタンスは必ずしも実行可能とは限らないことに注意してください。スキーマ定義のみを含み、リゾルバーがアタッチされていない場合もあります。

以下は、graphql-toolsmakeExecutableSchema関数を使用した簡単な例です。

typeDefsには、必須のQueryとシンプルなUser型を含むスキーマ定義が含まれています。resolversは、Query型で定義されたuserフィールドの実装を含むオブジェクトです。

makeExecutableSchemaは、スキーマ定義のSDL型からのフィールドを、resolversオブジェクトで定義された対応する関数にマッピングします。これにより、GraphQLSchemaのインスタンスが返され、実際のGraphQLクエリを*実行*するために使用できます。たとえば、GraphQL.jsのgraphql関数を使用します。

graphql関数はGraphQLSchemaのインスタンスに対してクエリを*実行*できるため、*GraphQL(実行)エンジン*とも呼ばれます。

GraphQL実行エンジンは、実行可能スキーマとクエリ(またはmutation)が与えられた場合に、有効なレスポンスを生成するプログラム(または関数)です。したがって、その主な責任は、実行可能スキーマ内のリゾルバー関数の呼び出しを調整し、GraphQL仕様に従ってレスポンスデータを適切にパッケージ化することです。

その知識を踏まえ、既存のGraphQL APIに基づいてGraphQLSchemaの実行可能なインスタンスを作成する方法について掘り下げていきましょう。

GraphQL APIのイントロスペクション

GraphQL APIの便利な特性の1つは、*イントロスペクション*を許可することです。これは、いわゆる*イントロスペクションクエリ*を送信することで、任意のGraphQL APIの*スキーマ定義*を抽出できることを意味します。

上記の例を考慮すると、次のクエリを使用して、スキーマからすべての型とそのフィールドを抽出できます。

これにより、次のJSONデータが返されます。

ご覧のとおり、このJSONオブジェクトの情報は、上記のSDLベースのスキーマ定義と同等です(実際には、フィールドの*引数*を要求していないため、100%同等ではありませんが、上記のイントロスペクションクエリを拡張してこれらを含めることもできます)。

リモートスキーマの作成

既存のGraphQL APIのスキーマをイントロスペクションする機能により、スキーマ定義が既存のものと同一である新しいGraphQLSchemaインスタンスを簡単に作成できます。それはまさにgraphql-toolsmakeRemoteExecutableSchemaのアイデアです。

makeRemoteExecutableSchemaは2つの引数を受け取ります。

  • *スキーマ定義*(上記のイントロスペクションクエリを使用して取得できます)。スキーマ定義は、実行時にイントロスペクションクエリを送信する(大きなパフォーマンスオーバーヘッドが発生する)のではなく、開発時に既にダウンロードし、.graphqlファイルとしてサーバーにアップロードすることがベストプラクティスと見なされていることに注意してください。
  • プロキシされるGraphQL APIに接続されている*Link*。本質的に、このLinkは、クエリとmutationを既存のGraphQL APIに転送できるコンポーネントです。そのため、その(HTTP)エンドポイントを知る必要があります。

makeRemoteExecutableSchema実装は、ここから非常に簡単です。スキーマ定義は、新しいスキーマの基礎として使用されます。しかし、リゾルバーはどうでしょうか、それらはどこから来るのでしょうか?

明らかに、スキーマ定義をダウンロードするのと同じ方法でリゾルバーを*ダウンロード*することはできません。リゾルバーのイントロスペクションクエリはありません。ただし、言及されたLinkコンポーネントを使用して、受信したクエリまたはmutationを基盤となるGraphQL APIに*転送*するだけの*新しい*リゾルバーを作成できます。

おしゃべりは十分です、コードを見てみましょう!以下は、Userと呼ばれる型のGraphcool CRUD APIに基づいて、専用サーバー(graphql-yogaを使用)を通して公開されるリモートスキーマを作成する例です。

このコードの動作例はこちらをご覧ください。

参考までに、User型のCRUD APIはこれと多少似ています(完全版はこちらで確認できます)。

リモートスキーマの裏側

上記の例のdatabaseServiceSchemaDefinitiondatabaseServiceExecutableSchemaが内部でどのように見えるか調べてみましょう。

GraphQLスキーマの検査

最初に注意すべき点は、どちらもGraphQLSchemaのインスタンスであるということです。ただし、databaseServiceSchemaDefinitionはスキーマ定義のみを含み、databaseServiceExecutableSchemaは実際には実行可能なスキーマです。つまり、型フィールドにリゾルバー関数がアタッチされています。

Chromeデバッガーを使用すると、databaseServiceSchemaDefinitionが次のようなJavaScriptオブジェクトであることがわかります。

A non-executable instance of GraphQLSchemaGraphQLSchemaの非実行可能インスタンス

青い四角形は、そのプロパティを持つQuery型を示しています。予想どおり、allUsersというフィールド(他にも)があります。ただし、このスキーマインスタンスでは、Queryのフィールドにリゾルバーがアタッチされていないため、実行可能ではありません。

databaseServiceExecutableSchemaも見てみましょう。

Executable Schema = Schema definition + Resolvers実行可能スキーマ = スキーマ定義 + リゾルバー

このスクリーンショットは、先ほど見たものと非常によく似ています。ただし、allUsersフィールドには、このresolve関数がアタッチされている点が異なります。(これは、Query型(Usernodeuser_allUsersMeta)の他のフィールドにも当てはまりますが、スクリーンショットでは見えません。)

さらに一歩進んで、resolve関数の実装を実際に見てみましょう(このコードはmakeRemoteExecutableSchemaによって動的に生成されたことに注意してください)。

12〜16行目が私たちにとって興味深いものです。fetcherと呼ばれる関数が、queryvariablescontextの3つの引数で呼び出されます。fetcherは、以前に提供したLinkに基づいて生成されました。基本的には、GraphQL操作を特定のエンドポイント(Linkの作成に使用されたエンドポイント)に送信できる関数であり、それはまさにここで実行していることです。13行目のクエリの値として渡される実際のGraphQLドキュメントは、リゾルバーに渡されたinfo引数(10行目を参照)に由来することに注意してください。infoには、クエリのAST表現が含まれています。

ルート以外のリゾルバーはネットワーク呼び出しを行わない

上記でallUsersルートフィールドのリゾルバー関数を調べたのと同じ方法で、User型のフィールドのリゾルバーがどのように見えるかを調べることもできます。したがって、databaseServiceExecutableSchema_typeMapsプロパティに移動する必要があります。そこで、フィールドを持つUser型を見つけることができます。

The User type has two fields: id and name (both have an attached resolver function)User型には、idとnameの2つのフィールドがあります(どちらにもリゾルバー関数がアタッチされています)。

両方のフィールド(idname)にはresolve関数がアタッチされています。以下は、makeRemoteExecutableSchemaによって生成されたそれらの実装です(両方のフィールドで同一であることに注意してください)。

興味深いことに、今回生成されたリゾルバーはfetcher関数を使用していません。実際には、ネットワークへの呼び出しは全く行いません。返される結果は、関数に渡されるparent引数(10行目)から単純に取得されます。

リモートスキーマでのリゾルバーデータのトレース

リモート実行可能スキーマのリゾルバーのトレースデータもこの発見を裏付けています。次のスクリーンショットでは、より深くネストされたクエリを送信できるように、以前のスキーマ定義をArticle型とComment型(それぞれがexistingUserにも接続されている)で拡張しました。

GraphQL Playgrounds support displaying tracing data for resolvers out-of-the-box (bottom right)GraphQL Playgroundは、トレースデータのリゾルバーの表示を標準でサポートしています(右下)。

トレースデータから、ルートリゾルバー(allUsersフィールドの場合)のみが顕著な時間(167ミリ秒)を要することが非常に明らかです。ルート以外のフィールドのデータを返す責任がある残りのすべてのリゾルバーは、実行に数マイクロ秒しかかかりません。これは、以前に行った観測、つまりルートリゾルバーは受信したクエリを転送するためにfetcherを使用し、ルート以外のすべてのリゾルバーは受信したparent引数に基づいてデータを単純に返すことで説明できます。

リゾルバーストラテジー

スキーマ定義のリゾルバー関数を実装する場合、これにアプローチする方法は複数あります。

標準パターン:タイプレベルリゾルブ

次のスキーマ定義を考えてみましょう。

Query型に基づいて、次のクエリをAPIに送信することが可能です。

対応するリゾルバーは通常どのように実装されるでしょうか?これに対する標準的なアプローチは次のようになります(このコードでfetchで始まる関数はデータベースからリソースをロードすると仮定します)。

このアプローチでは、*タイプレベル*でリゾルブしています。これは、特定のクエリ(例:特定のArticle)の実際のオブジェクトが、Article型のリゾルバーが呼び出される*前に*フェッチされることを意味します。

上記のクエリのリゾルバー呼び出しを考えてみましょう。

  1. Query.userリゾルバーが呼び出され、データベースから特定のUserオブジェクトをロードします。クエリでリクエストされていない場合でも、idnameを含むUserオブジェクトのすべてのスカラーフィールドをロードすることに注意してください。ただし、articlesについてはまだ何もロードしません。これは、次のステップで起こることです。
  2. 次に、User.articlesリゾルバーが呼び出されます。入力引数parentは前のリゾルバーからの戻り値であるため、リゾルバーがUseridにアクセスしてそれに対するArticleオブジェクトをロードできる完全なUserオブジェクトであることに注意してください。

この例に従うのが難しい場合は、GraphQLスキーマに関する前回の記事を必ずお読みください。

リモート実行可能スキーマは、マルチレベルリゾルバーアプローチを使用します

リモートスキーマの例とそのリゾルバーについてもう一度考えてみましょう。リモート実行可能スキーマを使用してクエリを実行すると、データソースはルートリゾルバー(上記のスクリーンショットでfetcherを見つけた場所)で*1回*のみヒットすることを学びました。他のすべてのリゾルバーは、受信したparent引数(最初のルートリゾルバー呼び出しの結果のサブパート)に基づいて正規の結果のみを返します。

しかし、それはどのように機能するのでしょうか?ルートリゾルバーは、必要なすべてのデータを単一のリゾルバーでフェッチするように見えます。しかし、これは非常に非効率的ではないでしょうか?確かに、常にすべてのリレーショナルデータ*を含む*すべてのオブジェクトフィールドをロードする場合、非常に非効率的になります。では、受信したクエリで指定されたデータのみをロードするにはどうすればよいでしょうか?

これが、リモート実行可能スキーマのルートリゾルバーが、クエリ情報を含む使用可能なinfo引数を利用する理由です。実際のリゾルバーの選択セットを見ることで、リゾルバーはオブジェクトのすべてのフィールドをロードする必要はなく、代わりに必要なフィールドのみをロードします。この「トリック」は、単一のリゾルバーですべてのデータをロードすることを依然として効率的にしています。

まとめ

この記事では、graphql-toolsmakeRemoteExecutableSchemaを使用して、既存のGraphQL APIの*プロキシ*を作成する方法を学びました。このプロキシは、*リモート実行可能スキーマ*と呼ばれ、自身のサーバー上で実行されます。これは、受信したクエリを基盤となるGraphQL APIに単純に転送します。

また、このリモート実行可能スキーマが、ネストされたデータがタイプレベルで複数回ではなく、最初のリゾルバーによって1回だけフェッチされる*マルチレベル*リゾルバーを使用して実装されていることも確認しました。

リモートスキーマについてまだ多くの発見があります。これはスキーマステッチングとどのように関係していますか?GraphQL subscriptionではどのように機能しますか?contextオブジェクトはどうなりますか?次に何を学びたいかコメント欄でお知らせください!👋

次回の投稿をお見逃しなく!

Prismaニュースレターに登録する