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

GraphQLを使い始めるとき、最初に尋ねるべき質問の1つは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つしかありませんが、スキーマ全体には3つのフィールドがあります。Query
型のルートフィールドuser、およびUser
型のid
フィールドとname
フィールドです。残りの2つのフィールドには、まだリゾルバーが必要です。ご覧のとおり、これらのリゾルバーの実装は簡単です。
クエリの実行
上記のクエリを考慮して、クエリがどのように実行され、データが収集されるかを理解しましょう。クエリには合計3つのフィールドが含まれています。user
(ルートフィールド)、id
、name
。これは、クエリがサーバーに到着すると、サーバーはフィールドごとに1つずつ、3つのリゾルバー関数を呼び出す必要があることを意味します。実行フローを見ていきましょう。

- クエリがサーバーに到着します。
- サーバーはルートフィールド
user
のリゾルバーを呼び出します。仮にfetchUserById
がこのオブジェクトを返すとしましょう。{ "id": "abc", "name": "Sarah" }
- サーバーは
User
型のフィールドid
のリゾルバーを呼び出します。このリゾルバーのroot
入力引数は、前の呼び出しからの戻り値であるため、単にroot.id
を返すことができます。 - 3と同様ですが、最終的には
root.name
を返します。(3と4は並行して発生する可能性があります。) - 解決プロセスが終了しました。最後に、結果は
data
フィールドでラップされ、GraphQL仕様に準拠します。
さて、user.id
とuser.name
のリゾルバーを自分で記述する必要はありますか?GraphQL.jsを使用する場合、実装が例のように簡単であれば、リゾルバーを実装する必要はありません。GraphQL.jsは、フィールドの名前とルート引数に基づいて、返す必要があるものをすでに推測しているため、実装を省略できます。
リクエストの最適化:DataLoaderパターン
上記で説明した実行アプローチでは、クライアントが深くネストされたクエリを送信すると、パフォーマンスの問題に非常に簡単に遭遇します。APIにも記事とコメントがあり、リクエストできると仮定して、このクエリを考えてみましょう。
特定のarticle
を特定のuser
からリクエストし、そのcomments
とそれらを書いたユーザーのname
をリクエストしていることに注意してください。
この記事に5つのコメントがあり、すべて同じユーザーによって書かれていると仮定しましょう。これは、writtenBy
リゾルバーを5回ヒットすることになりますが、毎回同じデータを返すだけです。DataLoaderを使用すると、このような状況で最適化して、N+1クエリの問題を回避できます。一般的なアイデアは、リゾルバー呼び出しがバッチ処理されるため、データベース(またはその他のデータソース)を1回だけヒットする必要があるということです。
DataLoaderの詳細については、Lee Byronによるこの優れたビデオをご覧ください。DataLoader — ソースコードウォークスルー(約35分)
GraphQL.js vs 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
を呼び出します。
これらのすべての関数の感覚をつかむには、この簡単なノードスクリプトを見てください。これは、簡単な例でそれらを使用しています。
graphql
関数は、構造と動作をすでに含むスキーマに対してGraphQLクエリを実行しています。graphql
の主な役割は、リゾルバー関数の呼び出しを調整し、提供されたクエリの形状に従って応答データをパッケージ化することです。その点で、graphql
関数によって実装された機能は、GraphQLエンジンとも呼ばれます。
graphql-tools
:インターフェースと実装の橋渡し
GraphQLを使用する利点の1つは、スキーマファースト開発プロセスを採用できることです。つまり、構築するすべての機能は、最初に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
は、どこかで柔軟性を犠牲にすることで、特定のワークフローを容易にします。素晴らしい「はじめに」エクスペリエンスを提供し、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
は、ほとんどのユースケースに対応し、優れた「はじめに」エクスペリエンスを提供する便利なレイヤーを上に実装するだけです。GraphQLスキーマを構築するためのより高度な要件がある場合にのみ、手袋を脱いでプレーンなGraphQL.jsを使用するのが理にかなっている場合があります。
次の記事では、ネットワーク層と、express-graphql、apollo-server、graphql-yogaなどのGraphQLサーバーを実装するためのさまざまなライブラリについて説明します。パート3では、GraphQLリゾルバーにおけるinfoオブジェクトの構造と役割について説明します。
次回の投稿をお見逃しなく!
Prismaニュースレターにサインアップ