2017年11月14日

GraphQLサーバーの基本:GraphQLスキーマ、TypeDefs & Resolversの解説

GraphQLサーバーの構造と実装(パートI)

GraphQL Server Basics

GraphQLを始めるにあたって、まず最初に問われることの一つは「どうやってGraphQLサーバーを構築するのか?」です。GraphQLは単に仕様として公開されているため、GraphQLサーバーは文字通り、好みのプログラミング言語で実装できます。

サーバーを構築し始める前に、GraphQLではスキーマを設計する必要があります。これがサーバーのAPIを定義します。この記事では、スキーマの主要なコンポーネントを理解し、実際に実装するメカニズムに光を当て、GraphQL.jsgraphql-toolsgraphene-jsといったライブラリがそのプロセスでどのように役立つかを学びます。

この記事では、純粋なGraphQLの機能のみに触れています。サーバーがクライアントとどのように通信するかを定義するネットワーク層の概念はありません。焦点は、「GraphQL実行エンジン」の内部動作とクエリ解決プロセスです。ネットワーク層については、次の記事を参照してください。

GraphQLスキーマはサーバーのAPIを定義する

スキーマの定義:スキーマ定義言語

GraphQLには、GraphQLスキーマを記述するために使用される独自の型言語があります。それがスキーマ定義言語(SDL)です。最もシンプルな形式では、GraphQL SDLを使って次のような型を定義できます

User型単独では、クライアントアプリケーションに何の機能も公開せず、アプリケーションにおけるユーザーモデルの構造を定義するだけです。APIに機能を追加するには、GraphQLスキーマのルート型であるQueryMutationSubscriptionにフィールドを追加する必要があります。これらの型は、GraphQL APIのエントリーポイントを定義します。

例えば、次のクエリを考えてみましょう

このクエリは、対応するGraphQLスキーマが、次のuserフィールドを持つQueryルート型を定義している場合にのみ有効です

したがって、スキーマのルート型が、サーバーによって受け入れられるクエリとミューテーションの形式を決定します。

GraphQLスキーマは、クライアントとサーバー間の明確な契約を提供します。

GraphQLSchemaオブジェクトはGraphQLサーバーの核となる

GraphQL.jsはFacebookによるGraphQLのリファレンス実装であり、graphql-toolsgraphene-jsといった他のライブラリの基盤を提供します。これらのライブラリのいずれかを使用する場合、開発プロセスは2つの主要なコンポーネントからなるGraphQLSchemaオブジェクトを中心に展開されます

  • スキーマの定義
  • リゾルバー関数形式での実際の実装

上記の例では、GraphQLSchemaオブジェクトは次のようになります

ご覧のように、スキーマのSDLバージョンは、GraphQLSchema型のJavaScript表現に直接変換できます。このスキーマにはリゾルバーがないため、実際のクエリやミューテーションを実行することはできません。詳細については、次のセクションで説明します。

リゾルバーはAPIを実装する

GraphQLサーバーにおける構造と振る舞い

GraphQLは、構造振る舞いが明確に分離されています。GraphQLサーバーの構造は、先ほど説明したように、サーバーの機能を抽象的に記述したスキーマです。この構造は、サーバーの振る舞いを決定する具体的な実装によって実現されます。実装の主要なコンポーネントは、いわゆるリゾルバー関数です。

GraphQLスキーマの各フィールドはリゾルバーによってサポートされています。

最も基本的な形式では、GraphQLサーバーはスキーマ内のフィールドごと1つのリゾルバー関数を持ちます。各リゾルバーは、自身のフィールドのデータをどのようにフェッチするかを知っています。GraphQLクエリはその本質において単なるフィールドの集合であるため、GraphQLサーバーが要求されたデータを収集するために実際に必要なことは、クエリで指定されたフィールドのすべてのリゾルバー関数を呼び出すことだけです。(これが、GraphQLが本質的にリモート関数を呼び出すための言語であるため、RPCスタイルのシステムと比較されることが多い理由でもあります。)

リゾルバー関数の構造

GraphQL.jsを使用する場合、GraphQLSchemaオブジェクトの型上の各フィールドには、resolve関数をアタッチできます。上記の例、特にQuery型上のuserフィールドを考えてみましょう。ここに、次のように単純なresolve関数を追加できます

fetchUserById関数が実際に利用可能で、Userインスタンス(idnameフィールドを持つJSオブジェクト)を返す場合、resolve関数はスキーマの実行を可能にします。

さらに深く掘り下げる前に、リゾルバーに渡される4つの引数を理解しましょう

  1. rootparentと呼ばれることもあります):GraphQLサーバーがクエリを解決するために必要なのは、クエリのフィールドのリゾルバーを呼び出すことだけだと述べたのを覚えていますか?ええ、それは幅優先(レベルごと)で行われ、各リゾルバー呼び出しのroot引数は、単に前回の呼び出しの結果です(特に指定がない限り、初期値はnullです)。
  2. args:この引数はクエリのパラメーターを運びます。この場合、フェッチされるUseridです。
  3. context:リゾルバーチェーンを介して渡されるオブジェクトで、各リゾルバーが読み書きできます(基本的にはリゾルバーが情報をやり取りし、共有するための手段です)。
  4. info:クエリまたはミューテーションのAST表現。詳細については、本シリーズのパートIII「GraphQLリゾルバーにおけるinfo引数の解明」で読むことができます。

先ほど、GraphQLスキーマの各フィールドはリゾルバー関数によってサポートされていると述べました。現時点ではリゾルバーが1つしかありませんが、私たちのスキーマ全体には、Query型のルートフィールドであるuserに加えて、User型にはidnameの3つのフィールドがあります。残りの2つのフィールドにはまだリゾルバーが必要です。ご覧の通り、これらのリゾルバーの実装は非常に簡単です

クエリ実行

上記のクエリを考慮して、それがどのように実行され、データが収集されるかを理解しましょう。クエリ全体には、userルートフィールド)、idnameの3つのフィールドが含まれています。これは、クエリがサーバーに到達したとき、サーバーはフィールドごとに3つのリゾルバー関数を呼び出す必要があることを意味します。実行フローを見ていきましょう

Blog image
  1. クエリがサーバーに到達します。
  2. サーバーはルートフィールドuserのリゾルバーを呼び出します。fetchUserByIdがこのオブジェクトを返すと仮定しましょう: { "id": "abc", "name": "Sarah" }
  3. サーバーはUser型上のidフィールドのリゾルバーを呼び出します。このリゾルバーのroot入力引数は、前回の呼び出しの戻り値なので、単純にroot.idを返すことができます。
  4. 3と同様ですが、最終的にroot.nameを返します。(3と4は並行して実行されることに注意してください。)
  5. 解決プロセスが終了し、最終的に結果はGraphQL仕様に準拠するようにdataフィールドでラップされます

さて、user.iduser.nameのリゾルバーを自分で書く必要が本当にあるのでしょうか?GraphQL.jsを使用する場合、実装が例のように些細なものであれば、リゾルバーを実装する必要はありません。GraphQL.jsはフィールド名とルート引数に基づいて何を返す必要があるかを既に推論するため、それらの実装を省略できます。

リクエストの最適化:DataLoaderパターン

上述の実行アプローチでは、クライアントが深くネストされたクエリを送信すると、パフォーマンスの問題に陥りやすくなります。私たちのAPIが、要求する記事コメントも付いており、このクエリを許可していると仮定します

特定のuserからの特定のarticle、さらにそのcommentsと、それらを作成したユーザーのnameを要求している点に注目してください。

この記事に5つのコメントがあり、すべて同じユーザーによって書かれたと仮定しましょう。これは、writtenByリゾルバーが5回ヒットされることになりますが、毎回同じデータが返されるだけです。DataLoaderは、このような状況でN+1クエリ問題を回避するために最適化を可能にします。一般的な考え方は、リゾルバー呼び出しがバッチ処理され、データベース(または他のデータソース)へのアクセスが1回だけで済むようにすることです。

DataLoaderの詳細については、Lee Byronによるこの優れた動画をご覧ください: DataLoader — ソースコードウォークスルー(約35分)

GraphQL.js 対 graphql-tools

次に、JavaScriptでGraphQLサーバーを実装するのに役立つ利用可能なライブラリについて話しましょう。主に、GraphQL.jsとgraphql-toolsの違いについてです。

GraphQL.jsはgraphql-toolsの基盤を提供する

最初に理解すべき重要なことは、GraphQL.jsがgraphql-toolsの基盤を提供しているということです。必要な型の定義、スキーマ構築、クエリの検証と解決といったすべての重い処理を行います。graphql-toolsは、GraphQL.jsの上に薄い利便性レイヤーを提供します。

GraphQL.jsが提供する関数を簡単に見ていきましょう。その機能は一般的にGraphQLSchemaを中心にしていることに注意してください

  • parsebuildASTSchema:GraphQL SDLで文字列として定義されたGraphQLスキーマが与えられた場合、これら2つの関数はGraphQLSchemaインスタンスを作成します: const schema = buildASTSchema(parse(sdlString))
  • validateGraphQLSchemaインスタンスとクエリが与えられた場合、validateはクエリがスキーマによって定義されたAPIに準拠していることを保証します。
  • executeGraphQLSchemaインスタンスとクエリが与えられた場合、executeはクエリのフィールドのリゾルバーを呼び出し、GraphQL仕様に従って応答を作成します。当然ながら、これはリゾルバーがGraphQLSchemaインスタンスの一部である場合にのみ機能します(そうでなければ、メニューはあるがキッチンがないレストランのようなものです)。
  • printSchemaGraphQLSchemaインスタンスを受け取り、SDL形式でその定義(文字列として)を返します。

GraphQL.jsで最も重要な関数はgraphqlであり、これはGraphQLSchemaインスタンスとクエリを受け取り、その後validateexecuteを呼び出すことに注意してください

これらの関数の全体像を把握するには、簡単な例でそれらを使用しているこのシンプルなnodeスクリプトをご覧ください。

graphql関数は、それ自体が既に構造振る舞いを含むスキーマに対してGraphQLクエリを実行します。したがって、graphqlの主な役割は、リゾルバー関数の呼び出しを調整し、提供されたクエリの形式に従って応答データをパッケージ化することです。その点において、graphql関数によって実装される機能は、GraphQLエンジンとも呼ばれます。

graphql-tools:インターフェースと実装の橋渡し

GraphQLを使用する利点の一つは、スキーマファーストの開発プロセスを採用できることです。つまり、構築するすべての機能はまずGraphQLスキーマに現れ、その後、対応するリゾルバーを通じて実装されます。このアプローチには多くの利点があります。例えば、バックエンド開発者が実際に実装する前に、フロントエンド開発者がモックAPIに対して作業を開始できるのは、SDLのおかげです。

GraphQL.jsの最大の欠点は、SDLでスキーマを記述し、その後GraphQLSchema実行可能なバージョンを簡単に生成できないことです。

上記で述べたように、parsebuildASTSchemaを使用してSDLからGraphQLSchemaインスタンスを作成できますが、これには実行を可能にする必要なresolve関数が欠けています!GraphQLSchemaを実行可能にする唯一の方法(GraphQL.jsを使用した場合)は、スキーマのフィールドにresolve関数を手動で追加することです。

graphql-toolsは、1つの重要な機能であるaddResolveFunctionsToSchemaでこのギャップを埋めます。これは、スキーマを作成するためのより良いSDLベースのAPIを提供するために非常に役立ちます。そして、それこそがgraphql-toolsmakeExecutableSchemaで行うことです

つまり、graphql-toolsを使用する最大の利点は、宣言型スキーマとリゾルバーを接続するための優れたAPIです!

graphql-toolsを使用しない場合とは?

先ほど、graphql-toolsが本質的にGraphQL.jsの上に利便性レイヤーを提供することを知りましたが、サーバーを実装する際にそれが適切な選択ではないケースはあるのでしょうか?

ほとんどの抽象化と同様に、graphql-toolsは、他の場所で柔軟性を犠牲にすることで、特定のワークフローをより簡単にします。驚くべき「Getting Started」体験を提供し、GraphQLSchemaを迅速に構築する際の摩擦を回避します。ただし、バックエンドによりカスタムな要件がある場合、例えばスキーマを動的に構築・変更する場合など、そのコルセットは少しきついかもしれません。その場合は、GraphQL.jsの使用に立ち返ることができます。

graphene-jsに関する簡単な注意点

graphene-jsは、Python版のアイデアを踏襲した新しいGraphQLライブラリです。これも内部的にはGraphQL.jsを使用していますが、SDLでのスキーマ宣言は許可していません。

graphene-jsは最新のJavaScript構文を深く取り入れ、クエリやミューテーションをJavaScriptクラスとして実装できる直感的なAPIを提供します。新しいアイデアでエコシステムを豊かにするGraphQLの実装がさらに増えるのは、非常に喜ばしいことです!

結論

この記事では、GraphQL実行エンジンのメカニズムと内部動作を解き明かしました。まず、サーバーのAPIを定義し、どのようなクエリやミューテーションが受け入れられ、応答形式がどうあるべきかを決定するGraphQLスキーマから始めました。次に、リゾルバー関数について深く掘り下げ、受信クエリを解決する際にGraphQLエンジンによって有効になる実行モデルを概説しました。最後に、GraphQLサーバーの実装に役立つ利用可能なJavaScriptライブラリの概要で締めくくりました。

この記事で議論された内容の実践的な概要を知りたい場合は、このリポジトリをご覧ください。異なるアプローチを比較するために、graphql-jsgraphql-toolsのブランチがあることに注目してください。

一般的に、GraphQL.jsがGraphQLサーバー構築に必要なすべての機能を提供していることに注意することが重要です。graphql-toolsは、ほとんどのユースケースに対応し、素晴らしい「Getting Started」体験を提供する利便性レイヤーをその上に実装しているに過ぎません。GraphQLスキーマ構築において、より高度な要件がある場合にのみ、素のGraphQL.jsを使用することを検討する意味があるかもしれません。

次の記事では、ネットワーク層と、express-graphqlapollo-servergraphql-yogaといったGraphQLサーバーを実装するための様々なライブラリについて議論します。パート3では、GraphQLリゾルバーにおけるinfoオブジェクトの構造と役割について取り上げます。

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

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

© . All rights reserved.