「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ

Web APIの新しい規格「GraphQL」の設計と実装について、藤吾郎(gfx)さんによる寄稿です。

「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ

この記事では、Web APIの規格のひとつであるGraphQL(グラフキューエル)について解説します。筆者gfxは2017年からGraphQL APIをプロダクションで運用しており、GraphQLのDX(Developer Experience) に大きな魅力を感じているソフトウェアエンジニアです。

GraphQLは、RESTful Web API(REST)の持つ問題を解決するために開発された規格です。そこで、この記事の前半では、RESTと比較しつつ、GraphQLの利点や欠点、そしてその機能の詳細を見ていきます。そして記事の後半では、筆者が使っているGraphQL Rubygraphql-rubyを題材に、RailsアプリにGraphQL APIを実装してみます。

※この記事はGraphQL仕様のバージョンJune 2018を元にしています。

GraphQLとは何か

まずGraphQLとは何でしょうか。GraphQLは、Facebookが開発しているWeb APIのための規格で、「クエリ言語」と「スキーマ言語」からなります。

1GraphQL | A query language for your API

クエリ言語は、GraphQL APIのリクエストのための言語で、これはさらにデータ取得系のquery、データ更新系のmutation、サーバーサイドからのイベントの通知であるsubscriptionの3種類があります。なお、この記事では、総称としてのクエリ言語は「クエリ言語」と書き、クエリの3種のひとつであるqueryは「query」と書くことにします。

スキーマ言語は、GraphQL APIの仕様を記述するための言語です。リクエストされたクエリは、スキーマ言語で記述したスキーマに従ってGraphQL処理系により実行されて、レスポンスを生成します。

GraphQLは、クエリがレスポンスデータの構造と似ていて情報量が多いこと、そしてスキーマによる型付けにより型安全な運用ができることが特徴となっています。

GraphQLのシンプルな例

ひとつ、GraphQLのスキーマとクエリのシンプルな例を挙げてみます。例えば、次のようなスキーマがあるとします。

type Query {
  currentUser: User!
}

type User {
  id: ID!
  name: String!
}

type Queryはqueryのための予約されたルートの型名で、ただひとつのフィールドcurrentUserを持ち、User!は「nullにならないUser型」という意味になります。また、type UserはnullにならないID型であるidフィールドと、nullにならないString型であるnameフィールドを持ちます。

実装はここでは触れませんが、ひとつひとつのフィールドはリゾルバresolverと呼ばれる関数がマッピングされます。リゾルバは、オブジェクト(例えばUserのインスタンス)を引数として受け取り、そのオブジェクトのプロパティ(例えばUser#nameを返すシンプルな関数です。

このスキーマに対して発行するクエリは、例えば次のようになります。

query GetCurrentUser {
  currentUser {
    id
    name
  }
}

これは「このクエリの名前はGetCurrentUserでクエリのタイプはquery(=データ取得系)currentUseridnameを取得する」という意味です。これに対応するレスポンスは、例えば次のようなJSONデータになるでしょう。

{
  "data": {
    "currentUser": {
      "id": "dXNlci80Mgo=",
      "name": "foo",
    }
  }
}

レスポンスとしては、スキーマに定義したフィールドのうち、クエリに指定したフィールドのみが返ってきています。また、クエリの構造とレスポンスデータの構造もよく似ています。このように、レスポンスに含まれるデータの指定が必須であること、クエリとレスポンスの構造がよく似ていることは、GraphQLの大きな特徴です。

この特徴により、GraphQLは優れたDXを持ち、ひいては生産性やクライアントコードの品質にもいい影響を与えます。例えば、クエリからレスポンスの構造を予測できるため、Web APIに対する深い知識がなくても、GraphQLのクエリであればある程度は読み書きができます。また、スキーマの情報を利用してクエリを書くためのサポートを行うクエリエディタが存在します。これにより、ことクエリの読み書きという点でいえば、学習コストも非常に少なくなっています。

GraphQLの起源とこれから

GrpahQLの詳細に入るまえに、その起源とこれからについて少しだけ触れておきます。

GraphQLは、もともとFacebookが開発したもので、2015年に仕様と参照実装がOSSになりました。GraphQLを開発する以前、FacebookはWeb APIとしてRESTful APIとFQL(Facebook Query Language)を運用していました。RESTful APIは現在も“Graph API”としてFacebookのソーシャルグラフへアクセスするための公開APIとして提供されています。しかし、SQL風の構文であるFQLはその後廃止されました。FQLはおそらくGraphQLに完全に置き換えられたのでしょう。

Facebookによれば、GraphQL開発の動機は、モバイルアプリケーションで利用するオブジェクトグラフとAPIのレスポンスの構造に乖離(かいり)があり、これを改善するためだったということです{$annotation_1}。GraphQLの特徴である「クエリの構造とレスポンスの構造がよく似ている」というのは、まさにGraphQLの開発の動機だったということになります。

そして2018年11月にGraphQL Foundationが設立されました。これまでFacebookで開発・運用されてきたGraphQLの規格そのものや、参照実装やエディタなどのツールチェインは、これからはGraphQL Foundationによりメンテナンスされていくことになります。今後についてはこれから決まっていく段階でしょうが、より安定して開発されることが期待できるのではないでしょうか。

2GraphQL Foundation

なぜGraphQLの採用が増えているのか

GraphQLが公開されて2018年で3年経ちました。いまやFacebook以外にも、GitHub、Airbnb、New York Times、Shopify、Atlassian、Netflixなど多くの企業が導入して、ツールチェインの開発や知見の公開をしてきました。特に、GitHubが2017年に公開した“GitHub API v4”がGraphQLを採用していたことで、国内でも急速に注目を集めはじめたという印象があります。

筆者がGraphQLに興味を持ったのもこのタイミングでした。筆者はGraphQL APIをKibelaというプロダクトに導入したのですが、このプロダクトで利用しているgrphql-rubyというライブラリは、まさにGitHubによって開発されているGraphQL処理系です。

ここでは、GraphQLの採用が増えている理由として利点を挙げていきます。また、欠点も同時に見ていきます。

GraphQLの利点

GraphQLがここまで急速に広まったのは、3つの理由があると考えています。ひとつはクエリとレスポンスの構造に対応関係があること、次にスキーマとその一部である型システムによりエディタにおける補完や型チェックなどのツールによる開発サポートが受けられること、そしてクエリの学習コストが低いことです。

クエリとレスポンスにおける構造の対応関係

まず第一に、クエリとレスポンスの構造に対応関係については前述のとおりです。これはクエリからレスポンスの構造を推測できるということでもありますし、逆にいえば求めるレスポンスに応じてクエリを書いていけるということです。GaphQLのリクエストは一見するとRESTful APIと比べて冗長になりますが、実際に開発で使ってみると情報量の多さはむしろ利点であると感じられます。

例えば、クライアントサイドのviewを作るときは、そのviewで必要な値をGraphQLのクエリの中にリストするだけで過不足なくリソースをリクエストできます。また、コードリーディングの際にWeb APIの詳細を知らなくても、ある程度クライアントサイドのコードを読み進められます。クエリとレスポンスの構造に対応関係があるというのは、これだけでGraphQLを採用したくなるほど強力な利点であるといえるでしょう。

スキーマとそれを利用するツールによる開発サポート

GraphQLはスキーマのあるWeb API規格です。このスキーマは、クエリやレスポンスの構造に加えて各々のフィールドの型を定義しています。これにより、スキーマ駆動開発{$annotation_2}が可能になり、またスキーマを利用したツールのサポートを受けられます。

例えば、GraphQL Foundationが提供する公式のツールに“GraphiQL”(グラフィクル)というIDEがあります。これはGraphQLに対してクエリを発行してレスポンスを閲覧するためのツール、つまりAPIコンソールです。しかし、ただクエリを発行するのみならず、スキーマを通じてクエリの補完やAPIリファレンスとの統合などの機能があるためIDE(統合開発環境)と呼ばれています。そしてその補完などの機能は、GraphiQLがスキーマの情報を利用することで実現しています。

このようなコンピューター言語に対するツールサポートは、language serviceと呼ばれます。GraphQLのlanguage serviceであるgraphql-language-serviceはGraphQL Foundationがメンテナンスしています。

クエリの学習コストが小さいこと

クエリの学習コストが非常に小さいことも、GraphQLの特筆すべき利点です。クエリのツールサポートも相まって、「必要なデータを取得する」というタスクがここまで簡単にできるものなのかと驚くほどです。このためGraphQLクライアントがなくてもHTTPリクエストさえできればWeb APIを使うのは簡単ですし、クライアントを自作するとしてもそれほど難しくありません。

実際、筆者のプロダクトでも、最初のGraphQLエンドポイントの追加から1年近くたってようやく専用クライアントのApolloを導入したのでした。それまでは、単にJSONをPOSTしてJSON responseを受け取るAPI endpointがひとつ増えただけという運用でした。

スキーマ言語については、ほとんどのコンポーネントがオブジェクト指向プログラミング言語と概念を共有している上、仕様自体も小さいため学習はそれほど難しくはないでしょう。

GraphQL API自体の実装コストは、GraphQL処理系ライブラリに依存する部分も大きいのですが、N+1問題の解決などの最適化などを後回しにするのであれば、それほど大きくもありません。むしろ、スキーマ言語というレールがあることにより、RESTよりも設計は簡単です。

ただし、GraphQLの処理系から作るのはかなり難しく、このことはGraphQLの欠点のひとつです。とはいえ、多くの言語ではすでにGraphQL処理系のライブラリがあるため、それらを使えば現実的なコストで開発・運用できるでしょう。

GraphQLの欠点

GraphQLの欠点もいくつかあります。いずれもブロッカーではないものの、解決の難しい問題ではあります。

パフォーマンスの分析が難しい

GraphQL APIのHTTPエンドポイントはひとつだけです。例えばNew RelicなどのAPM(Application Performance Management)でパフォーマンスの記録や分析などを行うときはエンドポイントごとに情報を収集するのが普通です。しかし、エンドポイントがひとつだけのGraphQLは全てのデータがまとめられてしまい、分析が困難です。

パフォーマンスの分析が難しいということは、現在のところ運用においては一番頭を悩ませている課題で、まだ解決していません。とはいえ、すでにプロダクションに導入しているサービスが多数あることから分かるとおり、ブロッカーというほどの問題でもありません。

もっとも、このことは、RESTful APIのようにひとつのURLをひとつのAPIに対応させたWeb APIが、現在のAPMの想定するWeb APIモデルと異なるからです。つまり、ツールの対応が追いついていないだけといえます。実際、プロプライエタリのPaaSであるApollo Platoformは、GraphQLのクエリを分析するApollo Engineというミドルウェアを提供しているようです。

GraphQL処理系を実装するのが難しい

GraphQLのクライアントをフルスクラッチで実装するのはそれほど難しくないのですが、GraphQLの処理系の実装コストはかなり大きなものです。したがって、多くのケースでは既存のライブラリを使うことになりますし、その場合はGraphQL APIの開発と運用コストはその採用したライブラリに大きく依存することになります。そのライブラリで未実装の機能は簡単には使えないという問題もあります。

採用するGraphQL処理系はOSSであれば、足りない機能のパッチを作ることもできます。しかし、この処理系を実装するのが難しいという問題は、シンプルなRESTful APIと比べて懸念点となることは否めません。

画像や動画などの大容量バイナリの扱いが難しい

大容量バイナリの送信(アップロード)も、GraphQLが苦手とするものです。

GraphQL自体はリクエストやレスポンスのシリアライズ方法は規定していないものの、多くのケースではJSONです。JSONはバイナリのシリアライズが苦手で、比較的エンコードサイズが軽量なBase64でもデータ量が1.3倍ほどになってしまいます。また、サーバーサイドのJSONでシリアライザは基本的にオンメモリなので、メモリに乗り切らないほど大きなデータはそもそも処理できません。

シリアライザをMessagePackなどバイナリシリアライザにして大容量バイナリをシリアライズ時にファイルに退避するなどの工夫は可能です。しかし、まさにその挙動がデフォルトであるHTTPのmultipart/form-dataと比較すると一手間かかってしまいます。

GraphQLを触ってみる

GraphQLの特徴の説明はこのくらいにして、実際にGraphQLを触ってみましょう。GitHubのアカウントがあればGitHub API v4を試すのが一番簡単です。

GitHub API v4について

GitHub API v4は、GitHubの公開Web APIです。

3GitHub GraphQL API v4 | GitHub Developer Guide

次のURLでAPIコンソール(GitHubは“API Explorer”と呼称)にアクセスできます。

4GraphQL API Explorer | GitHub Developer Guide

ここで操作するデータはGitHubの本番のデータなので、更新系のAPIを使うときは十分に気をつけてください。特に、他人のリポジトリにゴミデータを送信するようなことは絶対にしないように注意してください。

The GraphiQL IDEについて

GitHubのAPIコンソールはGraphiQLです。GraphiQLはGraphQLのための開発環境(IDE)で、補完やリアルタイム構文チェックなどを備えたエディタとスキーマから生成されたAPIリファレンスなどからなります。

例えば、GitHub API v4のGraphiQLは次のような外観です。

5

GraphQLではエディタ上でcontrol+enter(macOSの場合はcommand+enterでも可能)でクエリを実行し、その結果が右ペインに表示されます。また、エディタ上でcontrol+spaceを押すと、カーソル位置に応じて補完が出ます。

GitHub API v4のコンソールにはデフォルトで次のようなクエリが入力されているので、それを実行してみてください。

query {
  viewer {
    login
  }
}

すると、右のレスポンスペインにログインユーザ名を含む次のようなJSONが表示されるはずです。

{
  "data": {
    "viewer": {
      "login": "gfx"
    }
  }
}

また「Docs」はインクリメンタルサーチ付きのAPIリファレンスです。このAPIリファレンスはGraphQLスキーマから自動生成されたもので、ちょっとしたことを調べるにはこれで十分です。

GraphQLのコンポーネント

それでは、GraphQLのコンポーネントを見ていきましょう。GraphQLのコンポーネントは、スキーマ言語とクエリ言語からなります。

スキーマ言語は、リソース{$annotation_3}の単位であるtypeとその構成要素であるfieldが基本的な要素です。また、typeのバリエーションとして、interfaceとunion typeとenumがあります。また全体に関わる要素としてdirectiveとdescriptionがあります。

そしてクエリ言語は、query、mutation、subscriptionという3種類があり、それぞれデータ取得、データ更新、サーバーサイドイベントの購読となっています。

クエリは常にスキーマに対応するため、まずスキーマを見ていきます。

スキーマ言語 - GraphQLのコンポーネント その1

GraphQLのスキーマ言語は、Web APIの仕様を記述するための言語です。型システムを内包しており、クエリやレスポンスのバリデーションやリゾルバの適用のために使われます。

実際に設計する際は、GitHub API v4のリファレンスが参考になるでしょう。

6GitHub GraphQL API v4 | GitHub Developer Guide

Type

まずはtype、つまり型です。これはプログラミング言語における型やクラスに似ており、複数のfieldからなるリソースです。

ただし、プログラミング言語の型と違って実装はスキーマにはありません。typeはあくまでもリソースに対するインターフェイスを記述するだけです{$annotation_4}。

型はnull可能性の区別があり、デフォルトではnull可能性のある“nullable”で、typeのあとに!をつけるとnull可能性のない“non-nullable”になります。

また、型名を[]で囲むと配列になり、これもデフォルトはnullableで、!でnon-nullableになります。連想配列は組み込みでは用意されていません。

null可能性や配列について、まとめると次のようになります。

表示 説明
String nullableなString型
String! non-nullableなString型
[String] nullableなString型のnullableな配列型
[String!]! non-nullableなString型のnon-nullableな配列型

Field

次にfield(フィールド)です。それぞれのfieldは「名前: 型名」という構文で、必ず型を指定します。

最初に紹介したシンプルな例をもう一度見てみます。

type Query {
  currentUser: User!
}

type User {
  id: ID!
  name: String!
}

このスキーマは、currentUserというfieldを持つQueryというtypeと、id、nameというfieldを持つUserというtypeがあります。全ての型はnon-nullで、IDとStringは組み込み型です。

Queryはデータ取得系のクエリであるqueryのために予約されたtypeで、全てのqueryのルートとなるtypeです。

なお、fieldはさらに引数を設定できます。例えば、Relay Connectionというリスト型は、ページ送りのための引数を受け取ります。また、IDからリソースを取得するフィールドも引数を受け取ります。

例えば、IDからUserを取得するフィールドは次のようになるでしょう。これは、idに対応したUserを取得し、その型はnullableなので、おそらく対応するUserが存在しなければnullを返すのでしょう。

type Query {
  user(id: ID!): User
}

Interface

interface(インターフェイス)はtype同様に型の一種ですが、対応する具体的なリソースを持たない抽象型です。複数のtypeに共通フィールドをinterfaceとして抽出し、typeはinterfaceを“実装”できます。“実装”とはいうものの、typeそれ自体はスキーマとして実装を持つことはありません。

例えば、あとで紹介するRelay Server Specificationは次のようなinterfaceを持ち、これを実装した型であればIDから取得できるとしています。

interface Node {
  id: ID!
}

また、GitHub API v4では、「スターをつけられるリソース」としてStarrableというinterfaceを定義しています。

interfaceは複数のtypeの共通fieldがあるときには便利ですが、本来共通fieldのないtypeを無理にひとつのinterfaceにまとめる必要はありません。関連性のないtypeをまとめた抽象型が欲しいときは、次に紹介するunion型を使います。

Union

union(ユニオン)は「指定された複数の型のうち、いずれかの型」を示す抽象型です。例えば、union StringOrInt = String | Intというスキーマは「StringまたはIntのうち、いずれかの型」という意味のStringOrIntというunion型を定義します。

なお、複数のtypeのunion型をクエリで取得するときに必ずそれぞれの具象型ごとにconditional fragmentで書き分けて取得する必要があります。

例えば、次のようなスキーマがあるとします{$annotation_5}。

type Entry {
  id: ID!
  title: String!
  content: String!
}

type Comment {
  id: ID!
  content: String!
}

union SearchResult = Entry | Comment;

typ Query {
  search(q: String!): [SearchResult!]!
}

ここからは次のような仕様を読み取れます

  • queryとしてはsearchという文字列を引数にするfieldがあり、それはSearchResultというunion型の配列を返す
  • SearchResultはEntryとCommentというtypeのうちいずれかである

このsearch fiieldを使って検索結果を取得するときは、次のようにな ... on Type という構文でconditional fragmentを使い、EntryとCommentでそれぞれの型ごとにクエリを書きます。

query {
  # 文字列 "foo" で検索する
  search(q: "foo") {
    __typename # 全ての型にデフォルトで提供されるメタデータ。ここでは "Entry" または "Comment"

    # Entryの場合
    ... on Entry {
      id
      title
      content
    }

    # Commentの場合
    ... on Comment {
      id
      content
    }
  }
}

これは一見すると冗長です。しかし実際のアプリケーションでは、view componentにデータを渡すときにはリソースの型__typenameに応じてview componentを分けることになるでしょう。そのようなケースでは、クエリの段階で分岐しておくのはいいプラクティスであるといえます。

ここでは詳細には触れませんが、上記の例はinterfaceでも実現はできます。しかし、unionのほうがクエリの型ごとの分岐が明示的であるため後からでも読みやすく、またfragmentによってクエリを分割定義できるためメンテナンスはしやすいという感覚があります。

Scalar

scalar(スカラ)は、ただひとつの値からなる型です。組み込みのscalar型としては、次の表のものがあります。

型名 説明
Int 符号付き整数(32bit)
Float 浮動小数点数(64bit)
String 文字列(UTF-8)
Boolean 真偽値
ID 一意なID / 値としてはStringと同じ

scalar型を新しく定義するためにはscalarキーワードを使います。例えば、Date型を新しく定義するには次のようにします。

scalar Date

スキーマではこれだけですが、実際に使う際はGraphQL処理系に対してさらにシリアライズとデシリアライズを定義することになります。

GraphQL組み込みのscalar型は先にあげたものだけなので、例えばバイナリ、日付と時刻、HTML/XML、BigIntなどを必要に応じて追加することになるでしょう。ただしその場合、サーバーサイドとクライアントサイドでシリアライズ・デシリアライズの実装を一致させる必要があります。

Enum

enum(イナム)はscalar型の一種で、特定の値のみを持つ型です。例えば、組み込みscalar型であるBooleanをenumで宣言すると次のようになるでしょう。

enum Boolean {
  true
  false
}

ところで、enumで異なる構造を持つリソースをまとめるための「種類」を定義しないようにしてください。例えば、次のようなenumがあるとします。

enum DocumentType {
  ENTRY
  COMMENT
}

type Document {
  documentType: DocumentType!

  # COMMENTはtitleがないのでnullable
  title: String

  # ENTRYもCOMMENTもcontentは必ず存在するのでnon-nullable
  content: String!
}

これは、unionを自前で実装しているようなものです。そうではなく、次の例のようにそれぞれに応じたtypeとunionを用意するほうが、複数のtypeのインターフェイスを無理に一致させる必要がないため、適切にtypeを定義できます。unionをqueryで取り出すときには組み込みのfieldである__typenameが使えます。

type Entry {
  # ...
}

type Comment {
  # ...
}

union Document = Entry | Comment

Directive

directive(ディレクティブ)は、スキーマやクエリに対してメタデータを与えるための宣言です。directiveは処理系やツールによって解釈され、さまざまな効果を持ちます。directiveはスキーマ用のものとクエリ用のものがありますが、宣言方法は同じでターゲットを変えるだけです。

例えば、@deprecatedは、fieldが非奨励であることを示すための組み込みdirectiveで、次のように使います。

type T {
  newName: String!
  oldField: String! @deprecated(reason: "Use `newField` instead.")
}

この@deprecated directiveをつけられたfieldを使うことはできますが、GraphiQLのエディタで警告が出たり、GraphiQLのリファレンスマニュアルで表示されなかったりするなど、ツールが解釈して該当fieldを使わないように促してくれます。

この@deprecatedは次のように宣言されているのと等価です。実際には、組み込みなので宣言は不要ですが。

directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

なお、directiveのサポート状況はライブラリによって差があります。例えばgraphql-rubyは組み込みdirectiveのみサポートしていて、カスタムdirectiveはサポートしていません。活用すればロールベースの権限管理などに使えて便利なはずですが、ライブラリのサポートがなければ使うことは難しいので、筆者の環境では使っていません。

Description

descriptin(デスクリプション)は、typeやfieldなどに対する「説明」で、ツールから利用できるドキュメントです。例えば、GraphiQLが生成するリファレンスマニュアルにはこのdescriptionが表示されます。

descriptionはtypeやfieldの前に文字列リテラルとして書きます。また、descriptionの中身はmarkdown(CommonMark)を書けます。

"""
コメント型。必ずいずれかのEntry型に所属する。
"""
type Comment {

  "コメントの本文。フォーマットは[CommonMark](https://commonmark.org/)。"
  content: String!

  "コメントの本文。CommonMarkをHTMLに変換したもの。"
  contentHtml: String!
}

クエリ言語 - GraphQLのコンポーネント その2

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く