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

GraphQLを始めるにあたって、まず最初に問われることの一つは「どうやってGraphQLサーバーを構築するのか?」です。GraphQLは単に仕様として公開されているため、GraphQLサーバーは文字通り、好みのプログラミング言語で実装できます。
サーバーを構築し始める前に、GraphQLではスキーマを設計する必要があります。これがサーバーのAPIを定義します。この記事では、スキーマの主要なコンポーネントを理解し、実際に実装するメカニズムに光を当て、GraphQL.js、graphql-tools
、graphene-js
といったライブラリがそのプロセスでどのように役立つかを学びます。
この記事では、純粋なGraphQLの機能のみに触れています。サーバーがクライアントとどのように通信するかを定義するネットワーク層の概念はありません。焦点は、「GraphQL実行エンジン」の内部動作とクエリ解決プロセスです。ネットワーク層については、次の記事を参照してください。
GraphQLスキーマはサーバーのAPIを定義する
スキーマの定義:スキーマ定義言語
GraphQLには、GraphQLスキーマを記述するために使用される独自の型言語があります。それがスキーマ定義言語(SDL)です。最もシンプルな形式では、GraphQL SDLを使って次のような型を定義できます
User
型単独では、クライアントアプリケーションに何の機能も公開せず、アプリケーションにおけるユーザーモデルの構造を定義するだけです。APIに機能を追加するには、GraphQLスキーマのルート型であるQuery
、Mutation
、Subscription
にフィールドを追加する必要があります。これらの型は、GraphQL APIのエントリーポイントを定義します。
例えば、次のクエリを考えてみましょう
このクエリは、対応するGraphQLスキーマが、次のuser
フィールドを持つQuery
ルート型を定義している場合にのみ有効です
したがって、スキーマのルート型が、サーバーによって受け入れられるクエリとミューテーションの形式を決定します。
GraphQLスキーマは、クライアントとサーバー間の明確な契約を提供します。
GraphQLSchema
オブジェクトはGraphQLサーバーの核となる
GraphQL.jsはFacebookによるGraphQLのリファレンス実装であり、graphql-tools
やgraphene-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
インスタンス(id
とname
フィールドを持つJSオブジェクト)を返す場合、resolve
関数はスキーマの実行を可能にします。
さらに深く掘り下げる前に、リゾルバーに渡される4つの引数を理解しましょう
root
(parent
と呼ばれることもあります):GraphQLサーバーがクエリを解決するために必要なのは、クエリのフィールドのリゾルバーを呼び出すことだけだと述べたのを覚えていますか?ええ、それは幅優先(レベルごと)で行われ、各リゾルバー呼び出しのroot
引数は、単に前回の呼び出しの結果です(特に指定がない限り、初期値はnull
です)。args
:この引数はクエリのパラメーターを運びます。この場合、フェッチされるUser
のid
です。context
:リゾルバーチェーンを介して渡されるオブジェクトで、各リゾルバーが読み書きできます(基本的にはリゾルバーが情報をやり取りし、共有するための手段です)。info
:クエリまたはミューテーションのAST表現。詳細については、本シリーズのパートIII「GraphQLリゾルバーにおけるinfo引数の解明」で読むことができます。
先ほど、GraphQLスキーマの各フィールドはリゾルバー関数によってサポートされていると述べました。現時点ではリゾルバーが1つしかありませんが、私たちのスキーマ全体には、Query
型のルートフィールドであるuser
に加えて、User
型にはid
とname
の3つのフィールドがあります。残りの2つのフィールドにはまだリゾルバーが必要です。ご覧の通り、これらのリゾルバーの実装は非常に簡単です
クエリ実行
上記のクエリを考慮して、それがどのように実行され、データが収集されるかを理解しましょう。クエリ全体には、user
(ルートフィールド)、id
、name
の3つのフィールドが含まれています。これは、クエリがサーバーに到達したとき、サーバーはフィールドごとに3つのリゾルバー関数を呼び出す必要があることを意味します。実行フローを見ていきましょう

- クエリがサーバーに到達します。
- サーバーはルートフィールド
user
のリゾルバーを呼び出します。fetchUserById
がこのオブジェクトを返すと仮定しましょう:{ "id": "abc", "name": "Sarah" }
- サーバーは
User
型上のid
フィールドのリゾルバーを呼び出します。このリゾルバーのroot
入力引数は、前回の呼び出しの戻り値なので、単純にroot.id
を返すことができます。 - 3と同様ですが、最終的に
root.name
を返します。(3と4は並行して実行されることに注意してください。) - 解決プロセスが終了し、最終的に結果はGraphQL仕様に準拠するように
data
フィールドでラップされます
さて、user.id
とuser.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
を中心にしていることに注意してください
parse
とbuildASTSchema
:GraphQL SDLで文字列として定義されたGraphQLスキーマが与えられた場合、これら2つの関数はGraphQLSchemaインスタンスを作成します:const schema = buildASTSchema(parse(sdlString))
。validate
:GraphQLSchema
インスタンスとクエリが与えられた場合、validate
はクエリがスキーマによって定義されたAPIに準拠していることを保証します。execute
:GraphQLSchema
インスタンスとクエリが与えられた場合、execute
はクエリのフィールドのリゾルバーを呼び出し、GraphQL仕様に従って応答を作成します。当然ながら、これはリゾルバーがGraphQLSchema
インスタンスの一部である場合にのみ機能します(そうでなければ、メニューはあるがキッチンがないレストランのようなものです)。printSchema
:GraphQLSchema
インスタンスを受け取り、SDL形式でその定義(文字列として)を返します。
GraphQL.jsで最も重要な関数はgraphql
であり、これはGraphQLSchema
インスタンスとクエリを受け取り、その後validate
とexecute
を呼び出すことに注意してください
これらの関数の全体像を把握するには、簡単な例でそれらを使用しているこのシンプルなnodeスクリプトをご覧ください。
graphql
関数は、それ自体が既に構造と振る舞いを含むスキーマに対してGraphQLクエリを実行します。したがって、graphql
の主な役割は、リゾルバー関数の呼び出しを調整し、提供されたクエリの形式に従って応答データをパッケージ化することです。その点において、graphql
関数によって実装される機能は、GraphQLエンジンとも呼ばれます。
graphql-tools
:インターフェースと実装の橋渡し
GraphQLを使用する利点の一つは、スキーマファーストの開発プロセスを採用できることです。つまり、構築するすべての機能はまずGraphQLスキーマに現れ、その後、対応するリゾルバーを通じて実装されます。このアプローチには多くの利点があります。例えば、バックエンド開発者が実際に実装する前に、フロントエンド開発者がモックAPIに対して作業を開始できるのは、SDLのおかげです。
GraphQL.jsの最大の欠点は、SDLでスキーマを記述し、その後
GraphQLSchema
の実行可能なバージョンを簡単に生成できないことです。
上記で述べたように、parse
とbuildASTSchema
を使用してSDLからGraphQLSchema
インスタンスを作成できますが、これには実行を可能にする必要なresolve
関数が欠けています!GraphQLSchema
を実行可能にする唯一の方法(GraphQL.jsを使用した場合)は、スキーマのフィールドにresolve
関数を手動で追加することです。
graphql-tools
は、1つの重要な機能であるaddResolveFunctionsToSchema
でこのギャップを埋めます。これは、スキーマを作成するためのより良いSDLベースのAPIを提供するために非常に役立ちます。そして、それこそがgraphql-tools
がmakeExecutableSchema
で行うことです
つまり、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-js
とgraphql-tools
のブランチがあることに注目してください。
一般的に、GraphQL.jsがGraphQLサーバー構築に必要なすべての機能を提供していることに注意することが重要です。graphql-tools
は、ほとんどのユースケースに対応し、素晴らしい「Getting Started」体験を提供する利便性レイヤーをその上に実装しているに過ぎません。GraphQLスキーマ構築において、より高度な要件がある場合にのみ、素のGraphQL.jsを使用することを検討する意味があるかもしれません。
次の記事では、ネットワーク層と、express-graphql、apollo-server、graphql-yogaといったGraphQLサーバーを実装するための様々なライブラリについて議論します。パート3では、GraphQLリゾルバーにおけるinfoオブジェクトの構造と役割について取り上げます。
次の投稿をお見逃しなく!
Prismaニュースレターに登録する