2017.03.17

GraphQL Relay 対応サーバを Elixir で作る


graphql_relay_elixir_logo

次世代システム研究室の DevOps ネタ担当の M.Y. です。

前回の記事(Elixir & Phoenix は意外と広告向けサーバに向いてる?)では Elixir の話を書きました。今回もその延長で Dev 寄りの話です。

きっかけ:GraphQL を理解するのに苦労した話


最近、とあるプロジェクトで「React Native を使ってみよう」という意見が出て、「React Native なら、サーバ側は GraphQL にして、Relay 経由で叩きたいよね」という話になりました。

私の得意分野はサーバ側で、フロントエンドには詳しくないため、この時点でだいぶ「????」だったのですが、試しに Relay のサンプルコードを読んでもよくわからず、混乱するばかり……。

その後いろいろ勉強して GraphQL がどういうものかわかってきたのですが、最初に混乱した理由もわかってきました。

GraphQL をちゃんと理解する前に、Relay のコードを読んで GraphQL を理解しようとすると、以下のような理由で混乱すると思います。少なくとも私は混乱しました。
  • JavaScript のコード中に、GraphQL のクエリそのものは登場しない
  • JavaScript のコード中に、GraphQL のスキーマも登場しない
  • React のコンポーネントのなかに GraphQL の断片を記載する。Relay はそれらを元にして GraphQL クエリを暗黙的に作る
  • GraphQL の断片は、ES6 のテンプレートリテラルを使って Relay.QL` クエリ ` のように記載するが、この記法は GraphQL の文法と若干違う(fragment on …)
今回はこのあたりの経験を踏まえて、GraphQL とは何かという話から、実際に Elixir で GraphQL Relay 用サーバを作る方法まで解説したいと思います。

GraphQL とは


GraphQL とは、HTTP を通してデータの取得・変更を行うためのクエリ言語です。Facebook が提唱し、現在は Facebook 以外の企業の製品でも採用されています。

サーバ側では、GraphQL のための URL を1個だけ用意します(大抵は /graphql)。データを取得したいクライアントは、この URL に対して以下のようなクエリを送信します。

{
  user(id: 1) {
    name
    company {
      name
    }
  }
}

すると、以下のような応答が返されます。

{
  "user": {
    "name": "Alice"
    "company": {
      name: "GMO Internet, Inc."
    }
  }
}

どのようなクエリを実行することができるかは、スキーマとして定義します。例えば、上記のクエリを実行するためのスキーマは以下のようになります。

schema {
  query: Query
  # 通常は mutation も用意する
  # mutation: Mutation
}

# query operation で使えるフィールドの定義
type Query {
  user(id: ID!): User
}

# ユーザ情報
# name は登録必須、それ以外は任意
type User {
  id: ID!
  name: String!
  gender: String
  age: Int
  company: Company
}

# 会社情報
type Company {
  name: String
}

GraphQL は、RESTful API にはない、以下のような特長を持っています。
  • SQL のように、クライアント側が要求した情報だけを返せる。RESTful API で同様のことをしたい場合、必要なカラム名を URL パラメータなどで渡す必要があるが、GraphQL ではそれが仕様に含まれている。
    • 例えば、上記の例ではクライアントが User の gender や age を要求していないので、レスポンスに含まれていない。
  • RDBMS のようなフラットなデータだけでなく、JSON のように階層構造を持ったデータを一括で取得・変更することが可能。いわゆる N+1 問題を回避できる。
    • 上記の例で、もしも User の子要素にリスト(例えば hobbies)があれば、そのリストを一括で取得できる。
  • 事前に定義した GraphQL スキーマによって、クエリ構造や型の検査が行われる。ビルトインの型(Int, Float, String, Boolean, ID)に加えて、独自の型を定義できる。
    • 例えば、GraphQL には時刻を表す型がない。しかし、時刻を ISO8601 形式の文字列で表現する DateTime 型などを自分で定義できる。
実際に GraphQL を使う場合は、Qiita にある日本語の記事などで概要を掴んだら、あとは Facebook が公開している GraphQL の仕様書を最初から最後まで読むのが、結局は一番の近道かと思います。

それ以外では、graphql.org の Introduction to GraphQL が参考になりました。

Relay とは


Relay とは、React のために、データの取得・変更と、それらに付随した画面の更新を行うための JavaScript フレームワークです。Relay は、データの取得・更新を GraphQL で行うことを前提に作られています。

Relay は GraphQL を使うのですが、GraphQL の仕様が許すクエリすべてに対応しているわけではありません。Relay は特定の規約(convention)に従ったクエリのみを発行します。この規約は GraphQL Relay Specification として定義されています。

GraphQL Relay Specification には、以下の3種類の規約が含まれています。
  • Object Identification (仕様:Relay Global Object Identification Specification
    • id field を持つ Node interface を定義すること
    • 返り値に使う type が Node interface を実装すること
    • id field の値は globally unique であること
  • Connection (仕様:Relay Cursor Connections Specification
    • ページネーションのための規約
    • 複数の値を返すときは、edges field のなかに node field を含める
    • リストを操作させたい場合は、edges field のなかに cursor field を含める
    • リストの続きに関する情報を返したい場合は、pageInfo field を含める
  • Mutations (仕様:Relay Input Object Mutations Specification
    • データの変更のための規約
    • Mutation の名前は動詞にする
    • Mutation は、input という名前の引数1個のみを取る
    • 引数のための input type(suffix “Input”)と、返り値のためのtype(suffix “Payload”)を定義する
当然、GraphQL サーバ側は、この規約を理解して、応答する必要があります。説明だけ読んでもピンとこないと思うので、具体例として GitHub GraphQL API を試してみましょう。

GitHub の GraphQL API を使ってみる


α版の GitHub GraphQL API を試すことができる UI が、以下の URL で公開されています。GitHub アカウントを持っている人なら、誰でもログインして試せますので、GraphQL を試すにはうってつけです。

GraphQL API Explorer | GitHub Developer Guide
https://developer.github.com/early-access/graphql/explorer/

github_graphql_api_1

GitHub GraphQL API は、Relay のクエリにも対応しています。例えば、左上の入力欄に以下のクエリを入力し、実行してみてください。これは、自分が star を付けたリポジトリを取得するクエリです。

query { 
  viewer {
    starredRepositories(first: 2) {
      edges {
        node {
          id
          name
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

これを実行すると、例えば、以下のようなレスポンスが返されます。

{
  "data": {
    "viewer": {
      "starredRepositories": {
        "edges": [
          {
            "node": {
              "id": "MDEwOlJlcG9zaXRvcnkyNDA4NDczMA==",
              "name": "embulk"
            }
          },
          {
            "node": {
              "id": "MDEwOlJlcG9zaXRvcnkxOTE4Njc3",
              "name": "fluentd"
            }
          }
        ],
        "pageInfo": {
          "endCursor": "Y3Vyc29yOjMzNDY3NDg0",
          "hasNextPage": true
        }
      }
    }
  }
}

上記の endCursor を引数 after に渡して、再度クエリを実行すると、続きの2件を取得できます。このようなカーソルは、アプリ側で pagination を実装するために必要な機能です。

query { 
  viewer {
    starredRepositories(first: 2, after: "Y3Vyc29yOjMzNDY3NDg0") {
      edges {
        node {
          id
          name
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

以下はそのレスポンスです。

{
  "data": {
    "viewer": {
      "starredRepositories": {
        "edges": [
          {
            "node": {
              "id": "MDEwOlJlcG9zaXRvcnkxMDcyNTg=",
              "name": "autojump"
            }
          },
          {
            "node": {
              "id": "MDEwOlJlcG9zaXRvcnkzNzU0MjMzNA==",
              "name": "embulk-filter-insert"
            }
          }
        ],
        "pageInfo": {
          "endCursor": "Y3Vyc29yOjQxNDkzMzQ5",
          "hasNextPage": true
        }
      }
    }
  }
}

GraphQL の仕様が許す範囲で pagination を実現しようと思うと、スキーマ定義の方法は無数にありえます。みんながバラバラの方法で実装してしまうと理解が大変になるので、よく必要になるデータ操作のやり方を定めた規約が GraphQL Relay Specification というわけです。

Elixir で GraphQL Relay 対応サーバを作る


ここまでの知識を踏まえて、GraphQL Relay 対応サーバを Elixir で実装してみます。もちろん他の言語でも実装できます(というかそのほうが多分楽です)が、最近ちょうど Elixir を触っていたので Elixir でやってみました。

GraphQL ライブラリの選定


Elixir で GraphQL Relay 対応サーバを作る場合、以下の2つの選択肢があります。

今回は、スキーマ定義の読みやすさを重視して Absinthe を採用しました。ただ、場合によっては暗黙の(マクロの)知識が不要な graphql-elixir のほうが良いかもしれません。

スキーマの定義


mix phoenix.new で Phoenix アプリケーションを作ったあとで、web/schema.ex と web/schema/type.ex にスキーマを記載します。

ユーザ情報を取得するだけのサンプルを、以下の Gist にアップロードしましたので、詳細は以下の Gist をご覧ください。

サンプルコード:Sample of GraphQL Relay server on Phoenix (Gist)
https://gist.github.com/muziyoshiz/47ff80c7b4c96b8e9432e100b00b5c70

Absinthe の場合は、このように query, connection, node などを使って、スキーマを定義します。この例では、説明を簡単にするために認証処理を含めていませんが、Absinthe.Resolution のドキュメント を参考にすることで実装できました。

このようにスキーマを定義すると、Absinthe は GraqhQL クエリを送受信処理の一部を自動的に行ってくれます。また、GraphQL クライアントから GraphQL サーバに対してスキーマを要求することがある(この要求も GraphQL クエリとして送信される)のですが、このクエリの処理も Absinthe が肩代わりしてくれます。

動作確認


Absinthe の動作確認をするには、’absinthe_plug’ というライブラリを導入して、GraphiQL を使うのが一番簡単です。

まず、mix.exs に absinthe_plug と absinthe_relay を追加して、mix deps.get します。absinthe_plug を使う場合、absinthe_plug が absinthe に依存しているため、absinthe を書く必要はありません。

次に、router.ex に以下を追加します。

  scope "/" do
    if Mix.env == :dev do
      forward "/graphql", Absinthe.Plug.GraphiQL, schema: PhoenixRelaySample.Schema
    else
      forward "/graphql", Absinthe.Plug, schema: PhoenixRelaySample.Schema
    end
  end

そのうえで mix phoenix.server を実行し、http://localhost:4000/graphql に接続すると、GitHub の GraphQL API Explorer と同じような UI が表示されます。これは、どちらも GraphiQL(名前が紛らわしいですが、Graph “i” QL です)というツールを使っているためです。サーバ側の実装に問題がなければ、以下のようにクエリを実行できます。

graphql_sample

Chrome の開発者ツールなどを使って、GraphiQL のページを開いたときの HTTP リクエストを見てみると、/graphql に対して GraphQL スキーマを要求するクエリを送信していることがわかります。GraphiQL はこのスキーマ情報を使って Documentation Explorer の表示や、クエリの文法チェックを実行しています。

graphql_sample2

試してみてわかったこと


他のメンバに React Native 側を実装してもらって、このサーバと接続して気づいたのですが、JavaScript の Relay からアクセスするためには(Relay の仕様には明示されていませんが)ルートノードが必要なようです(参考:Support Root Fields w/o Node Mapping · Issue #112 · facebook/relay)。Absinthe では、以下のトリックを使うことで、query {} でアクセスできる型に query { relay {} } でもアクセスできるようになりました。

    @desc """
    Hack to workaround https://github.com/facebook/relay/issues/112 re-exposing the root query object
    """
    field :relay, :query do
      resolve (fn _parent, _arg, _info -> {:ok, %{}} end)
    end

また、Absinthe の connection 対応には一部制限があり、引数 first と before の組合せ、および引数 last と after の組合せには対応していないようです。調べてみたところ、GitHub の GraphQL API は、いずれの組合せにも対応していました。今回は、以下のように、Absinthe が対応していない組合せを拒否する処理を追加しました。

  def list(_parent, args, _info) do
    # Error messages same as that of GitHub GraphQL API
    case args do
      %{first: _first, last: _last} ->
        {:error, "Passing both `first` and `last` values to paginate the `users` connection is not supported."}
      %{first: _first, before: _before} ->
        {:error, "Passing `first` with `before` is not supported."}
      %{first: _first} ->
        do_list(args)
      %{last: _last, after: _after} ->
        {:error, "Passing `last` with `after` is not supported."}
      %{last: _last} ->
        do_list(args)
      _ ->
        {:error, "You must provide a `first` or `last` value to properly paginate the `users` connection."}
    end
  end

とはいえ、普通に pagination を実装するだけなら、この制限は問題にならないと思います。

まとめ


今回は、RESTful API にはないメリットを持つ API である GraphQL と、GraphQL Relay の仕様について調べました。また、Elixir で GraphQL サーバを実装し、GraphiQL や、React Native で実装したアプリと実際に接続してみました。

一通り使ってみた感想としては、確かに RESTful API よりも GraphQL を使ったほうが便利な場面はありそうです。アプリのために開発されたという経緯がある通り、React および Relay と組み合わせた場合に、最もその威力を発揮すると思います。

GitHub の GraphQL API Explorer であればすぐ試せますので、もしもこの記事を読んで興味を持たれた方は、実際に叩いてみることをおすすめします。


次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。