2017年11月14日

GraphQLサーバーの基礎:GraphQLスキーマ、TypeDefsとリゾルバーについて解説

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

GraphQL Server Basics

GraphQLを使い始めるとき、最初に尋ねるべき質問の1つは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インスタンス(idフィールドとnameフィールドを持つJSオブジェクト)を返すことを前提とすると、resolve関数はスキーマの実行を可能にします。

さらに深く掘り下げる前に、リゾルバーに渡される4つの引数を理解するために少し時間をかけましょう。

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

先ほど、GraphQLスキーマの各フィールドはリゾルバー関数によってバックアップされると述べました。今のところ、リゾルバーは1つしかありませんが、スキーマ全体には3つのフィールドがあります。Query型のルートフィールドuser、およびUser型のidフィールドとnameフィールドです。残りの2つのフィールドには、まだリゾルバーが必要です。ご覧のとおり、これらのリゾルバーの実装は簡単です。

クエリの実行

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

  1. クエリがサーバーに到着します。
  2. サーバーはルートフィールドuserのリゾルバーを呼び出します。仮にfetchUserByIdがこのオブジェクトを返すとしましょう。{ "id": "abc", "name": "Sarah" }
  3. サーバーはUser型のフィールドidのリゾルバーを呼び出します。このリゾルバーのroot入力引数は、前の呼び出しからの戻り値であるため、単にroot.idを返すことができます。
  4. 3と同様ですが、最終的にはroot.nameを返します。(3と4は並行して発生する可能性があります。)
  5. 解決プロセスが終了しました。最後に、結果はdataフィールドでラップされ、GraphQL仕様に準拠します。

さて、user.iduser.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を中心に展開されることに注意してください。

  • 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を呼び出します。

これらのすべての関数の感覚をつかむには、この簡単なノードスクリプトを見てください。これは、簡単な例でそれらを使用しています。

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

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

GraphQLを使用する利点の1つは、スキーマファースト開発プロセスを採用できることです。つまり、構築するすべての機能は、最初に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は、どこかで柔軟性を犠牲にすることで、特定のワークフローを容易にします。素晴らしい「はじめに」エクスペリエンスを提供し、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-graphqlapollo-servergraphql-yogaなどのGraphQLサーバーを実装するためのさまざまなライブラリについて説明します。パート3では、GraphQLリゾルバーにおけるinfoオブジェクトの構造と役割について説明します。

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

Prismaニュースレターにサインアップ