この記事では、既存の*あらゆる*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-tools
のmakeExecutableSchema
関数を使用した簡単な例です。
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-tools
のmakeRemoteExecutableSchema
のアイデアです。
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はこれと多少似ています(完全版はこちらで確認できます)。
リモートスキーマの裏側
上記の例の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に基づいて生成されました。基本的には、GraphQL操作を特定のエンドポイント(Linkの作成に使用されたエンドポイント)に送信できる関数であり、それはまさにここで実行していることです。13行目のクエリの値として渡される実際のGraphQLドキュメントは、リゾルバーに渡されたinfo引数(10行目を参照)に由来することに注意してください。info
には、クエリのAST表現が含まれています。
ルート以外のリゾルバーはネットワーク呼び出しを行わない
上記でallUsers
ルートフィールドのリゾルバー関数を調べたのと同じ方法で、User
型のフィールドのリゾルバーがどのように見えるかを調べることもできます。したがって、databaseServiceExecutableSchema
の_typeMaps
プロパティに移動する必要があります。そこで、フィールドを持つUser
型を見つけることができます。
User型には、idとnameの2つのフィールドがあります(どちらにもリゾルバー関数がアタッチされています)。
両方のフィールド(id
とname
)にはresolve
関数がアタッチされています。以下は、makeRemoteExecutableSchema
によって生成されたそれらの実装です(両方のフィールドで同一であることに注意してください)。
興味深いことに、今回生成されたリゾルバーはfetcher
関数を使用していません。実際には、ネットワークへの呼び出しは全く行いません。返される結果は、関数に渡されるparent
引数(10行目)から単純に取得されます。
リモートスキーマでのリゾルバーデータのトレース
リモート実行可能スキーマのリゾルバーのトレースデータもこの発見を裏付けています。次のスクリーンショットでは、より深くネストされたクエリを送信できるように、以前のスキーマ定義をArticle
型とComment
型(それぞれがexistingUser
にも接続されている)で拡張しました。
GraphQL Playgroundは、トレースデータのリゾルバーの表示を標準でサポートしています(右下)。
トレースデータから、ルートリゾルバー(allUsersフィールドの場合)のみが顕著な時間(167ミリ秒)を要することが非常に明らかです。ルート以外のフィールドのデータを返す責任がある残りのすべてのリゾルバーは、実行に数マイクロ秒しかかかりません。これは、以前に行った観測、つまりルートリゾルバーは受信したクエリを転送するためにfetcher
を使用し、ルート以外のすべてのリゾルバーは受信したparent
引数に基づいてデータを単純に返すことで説明できます。
リゾルバーストラテジー
スキーマ定義のリゾルバー関数を実装する場合、これにアプローチする方法は複数あります。
標準パターン:タイプレベルリゾルブ
次のスキーマ定義を考えてみましょう。
Query
型に基づいて、次のクエリをAPIに送信することが可能です。
対応するリゾルバーは通常どのように実装されるでしょうか?これに対する標準的なアプローチは次のようになります(このコードでfetch
で始まる関数はデータベースからリソースをロードすると仮定します)。
このアプローチでは、*タイプレベル*でリゾルブしています。これは、特定のクエリ(例:特定のArticle
)の実際のオブジェクトが、Article
型のリゾルバーが呼び出される*前に*フェッチされることを意味します。
上記のクエリのリゾルバー呼び出しを考えてみましょう。
Query.user
リゾルバーが呼び出され、データベースから特定のUser
オブジェクトをロードします。クエリでリクエストされていない場合でも、id
やname
を含むUser
オブジェクトのすべてのスカラーフィールドをロードすることに注意してください。ただし、articles
についてはまだ何もロードしません。これは、次のステップで起こることです。- 次に、
User.articles
リゾルバーが呼び出されます。入力引数parent
は前のリゾルバーからの戻り値であるため、リゾルバーがUser
のid
にアクセスしてそれに対するArticle
オブジェクトをロードできる完全なUser
オブジェクトであることに注意してください。
この例に従うのが難しい場合は、GraphQLスキーマに関する前回の記事を必ずお読みください。
リモート実行可能スキーマは、マルチレベルリゾルバーアプローチを使用します
リモートスキーマの例とそのリゾルバーについてもう一度考えてみましょう。リモート実行可能スキーマを使用してクエリを実行すると、データソースはルートリゾルバー(上記のスクリーンショットでfetcher
を見つけた場所)で*1回*のみヒットすることを学びました。他のすべてのリゾルバーは、受信したparent
引数(最初のルートリゾルバー呼び出しの結果のサブパート)に基づいて正規の結果のみを返します。
しかし、それはどのように機能するのでしょうか?ルートリゾルバーは、必要なすべてのデータを単一のリゾルバーでフェッチするように見えます。しかし、これは非常に非効率的ではないでしょうか?確かに、常にすべてのリレーショナルデータ*を含む*すべてのオブジェクトフィールドをロードする場合、非常に非効率的になります。では、受信したクエリで指定されたデータのみをロードするにはどうすればよいでしょうか?
これが、リモート実行可能スキーマのルートリゾルバーが、クエリ情報を含む使用可能なinfo引数を利用する理由です。実際のリゾルバーの選択セットを見ることで、リゾルバーはオブジェクトのすべてのフィールドをロードする必要はなく、代わりに必要なフィールドのみをロードします。この「トリック」は、単一のリゾルバーですべてのデータをロードすることを依然として効率的にしています。
まとめ
この記事では、graphql-tools
のmakeRemoteExecutableSchema
を使用して、既存のGraphQL APIの*プロキシ*を作成する方法を学びました。このプロキシは、*リモート実行可能スキーマ*と呼ばれ、自身のサーバー上で実行されます。これは、受信したクエリを基盤となるGraphQL APIに単純に転送します。
また、このリモート実行可能スキーマが、ネストされたデータがタイプレベルで複数回ではなく、最初のリゾルバーによって1回だけフェッチされる*マルチレベル*リゾルバーを使用して実装されていることも確認しました。
リモートスキーマについてまだ多くの発見があります。これはスキーマステッチングとどのように関係していますか?GraphQL subscriptionではどのように機能しますか?context
オブジェクトはどうなりますか?次に何を学びたいかコメント欄でお知らせください!👋
次回の投稿をお見逃しなく!
Prismaニュースレターに登録する