RejectKaigi 2017で「GraphQL on Rails」という発表をしました

id:gfxです。 RejectKaigi 2017で「GraphQL on Rails」という発表をしました。
当日はKibelaでプレゼンテーションをしたので、それをそのまま資料として発表します。

2017/08/19 at Speee, Roppongi.

自己紹介

  • Kibelaという情報共有サービスを開発している
    • Kibelaの公開Web APIをフルスクラッチするにあたってGraphQLを調べているところ
  • 自分のOSSを誰かに引き継いだり共同開発体制にしていたら、GitHubのpinned reposがすべてorganizationのものになった

本日の話

  • GraphQLとは
  • GraphQLをRailsに組み込む話

GraphQLとは

  • Web API のためのクエリ言語
  • RESTful API の次を狙うWeb APIアーキテクチャという位置づけ
    • RPC的な発想にRESTfulのリソース指向で味付けしたものともいえる
  • Graph DBとは関係ない
  • SQL / RDBMS とも関係ない

Real world GraphQL API

追記:

クエリ例: user “gfx” のpinned reposの名前を取得する

request:
query {
  user(login: "gfx") {
    pinnedRepositories(first: 2) {
      nodes {
        nameWithOwner
      }
    }
  }
}
response:
{
  "data": {
    "user": {
      "pinnedRepositories": {
        "nodes": [
          {
            "nameWithOwner": "maskarade/Android-Orma"
          },
          {
            "nameWithOwner": "maskarade/gradle-android-ribbonizer-plugin"
          }
        ]
      }
    }
  }
}
  • query root typeの下に userrepositories などのフィールドが生えている
    • フィールドはRESTでいうところののリソース
  • リクエストとしたのと同じ構造のJSONが取得できるのが大きな特徴
  • つまりGraphQLはJSON responseを前提としたWeb API アーキテクチャといえる
    • これに対してRESTful APIはJSON responseを前提としていないアーキテクチャだった
    • またRESTだと配列型のレスポンスをJSON Arrayにするかラッパーオブジェクトでくるむかという流派によるスタイルの違いがあったが、GraphQLはresponseの構造が厳密に定められている

例2: repositoryにstarをつける

request:
# starrableIdの取得例:
#
# query  {
#  repository(owner:"gfx", name:"RxJavaExample") {
#    id
#  }
# }
mutation {
  addStar(input: { starrableId:"MDEwOlJlcG9zaXRvcnkzODY5Mzc4NA==" }) {
    starrable {
      id
    }
  }
}
response:
{
  "data": {
    "addStar": {
      "starrable": {
        "id": "MDEwOlJlcG9zaXRvcnkzODY5Mzc4NA=="
      }
    }
  }
}
  • mutation root typeの下に addStarcreateProject, updateProject などの更新系のフィールドが生えている
  • このへんはただのRPCといえる

GraphQLのPros / Cons

主にRESTful APIとの比較。

Pros

  • 特定のフィールド(たとえば Blog#title )のみリクエストして負荷を減らせる、というかリクエストするフィールドを常にすべて明示する設計
    • RESTful APIでも一部のフレームワーク(たとえば the_garage gem)は fields= という同じことができる仕組みを持っていたりする
  • GraphQLはスキーマが仕様に組み込まれている
    • RESTful API にスキーマを当てる試みはいくつかあって、それはわりとうまくいったりする
  • GraphQLのAPI console GraphiQL (グラフィクル)が優秀
    • RESTful APIにスキーマをあてるフレームワークにも同様のAPI consoleがあったりはする

Cons

  • HTTPにまつわる知見の多くが活用できない
    • method, status code, cache, monitoring…
    • 認証などは利用できる
    • このへんはRPC over HTTPと全く同じ様相
  • 新しすぎて情報が乏しい
  • 技術的に枯れてない
  • 静的型付き言語との相性がイマイチよくない

Railsアプリに組み込む

実装例 (Kibelaのコードを一部改変):
# 例:
#
# /api/v1 - API
# /api/v1/console - API console (GraphiQL グラフィクル という実装がある)

scope :api do
  scope :v1 do
    post "/", to: "graphql#execute"
    mount GraphiQL::Rails::Engine, at: "/console", graphql_path: "/api/v1"
  end
end
# 実際のコードは例外処理がある
class GraphqlController < ApplicationController
  def execute(query:, variables: {})
    # GraphQLのvariablesはSQLでいうところのnamed placeholdersとparameters
    result = KibelaSchema.execute(query, variables: variables, context: context)
    render json: result
  end

  private

  def context
    {
      current_user: current_user,
    }
  end
end
Types::QueryType = GraphQL::ObjectType.define do
  name "Query"
  description "The query root of the Kibela schema"

  field :user do
    type Types::UserType

    argument :id, !types.String

    description "Find a User by account"
    resolve ->(_object, args, _context) {
      User.alive.fond_by_args!(args)
    }
  end

  # ...
end
Types::UserType = GraphQL::ObjectType.define do
  name "User"
  description "A user is an indivisual account of a team"

  interfaces [GraphQL::Relay::Node.interface]

  global_id_field :id # キャッシュのために必要

  field :id, !types.ID, property: :relay_id # User#id ではなく全体でuniqueな値
  field :account, !types.String
  field :real_name, !types.String
  # ...
end
  • IDはglobal unique idにするのがベストプラクティスとのこと
  • global idをキャッシュに使う話: http://graphql.org/learn/caching/
    • GraphQL clientのRelayで使われるらしい

GrahphQL on Railsの所感

  • モデル(ActiveRecord)へのインターフェイスとして宣言的に記述する
  • 思想や用語がRESTful APIとまったく違うので学習コストは高い
  • RubyMineと相性が悪く、GraphQLの型は定義元ジャンプなどを利用できない
  • RESTfulに比べて判断しなければならないことが圧倒的に少ないと感じる
  • 🌟型がある安心感はすばらしい🌟

GraphQLで悩むポイント

…の前に、まず前提の共有を。
  • 「GraphQL vs RESTful API 」と捉えるとよい
    • 広い目で見ると「RPC vs RESTFUL API」の議論の一種ともいえそう
  • 用語(たとえば「N+1問題」)が人によって微妙に定義が違うので議論の際ははっきりさせること
  • 公開用API と 非公開API だと少し使われ方が違うのでそこも議論のポイント
  • 「GraphQLの設計」と「実装やエコシステムの成熟度」は分けて考えたほうがよい
    • 現在の実装でもってGraphQLそのものの良し悪しの議論はすべきではない
    • 実装や手法が枯れていないというのは紛れもなく事実ではある

N+1 にまつわる問題

  • まず問題を正確に定義しよう
    • HTTPリクエストのN+1
    • SQLのN+1

まずRESTful APIのケースを考える

  • N+1 のHTTPリクエストが必要になるのはよくあるケース
  • 設計次第でN+1が不要なAPIは作れる
  • 途中から N+1 が不要なAPIを作ったとして、問題のあるN+1をやめさせるにはどうしたらいいか?
    • ⇢API開発者がクライアントサイドのチーム(あるいは外部のAPIユーザ!)とコミュニケーションをとって修正してもらうことになる

GraphQLだとどうか?

  • 基本的にクエリ一発でとってくるものなので、HTTPリクエストは1回
  • もちろんそのままだとRDBMSへのN+1リクエストは起きる
  • N+1 を解決するには、サーバーサイドでGraphQL ASTを解析してN+1を解消するようなクエリにする(ActiveRecordだと #preload#includes などを使うことになる)
  • クライアントサイドの変更は必要なし
  • パフォーマンス的に問題でなければそもそもこの工夫すら不要
    • それでもHTTPリクエストでN+1するよりだいぶマシ

公開APIのRate Limit

GitHub API v4は次の2つの制限をもつ:
  • Node Limit
    • node数を数えて制限
  • Rate Limit
    • RESTful APIとおなじくHTTP requestの制限
GitHubのnode数計算方法:

画像のアップロード

選択肢はいくつかあるので、どれを選ぶかはサービスの性質次第:
  • base64 encodeしてvariablesに突っ込む
  • MessgePackなどのbinaryを入れられるシリアライザでvariablesをシリアライズする
    • さっき思いついたばかりなので未検証。実用している例は見当たらなかった。
  • multipart/form-data でGraphQLとバイナリを同時に送る
  • GraphQLを諦めてそこだけRESTful APIにする

Naming

  • snake_case vs camelCase
  • 小さいところとはいえ判断の迷うところ
  • Rubyは snake_case
  • クライアントサイドの言語(JS, Java, Swift, …) は camelCase だから…
  • content_html をcamel case変換すると contentHtmlcontentHTML の2パターンある問題 💢
  • Kibelaをどうするかはまだ決めてない

モニタリング

  • エンドポイントごとのログがほぼ意味をなさなくなる
  • 基本的にはツールチェインの成熟を待つしかない
  • 自作するのであれば、GraphQLのASTを解析するなどが必要だが、技術的には可能

本日のまとめ

  • GraphQLは「RESTful APIの一部のベストプラクティスをガチガチに強制するもの」と捉えると理解しやすい
  • GraphQLそれ自体の話と現在の実装やツールチェインがどうかという話は分けて考えるべし
  • Railsとの相性は悪くないが、ベストプラクティスはまだ定まらない感
  • KibelaはWeb APIをGraphQLで公開します!

参考