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
- GitHub API v4はGraphQL
追記:
- public GraphQL APIを集めたエントリがあるみたいです
- A collective list of public GraphQL APIs
クエリ例: 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の下にuser
やrepositories
などのフィールドが生えている- フィールドは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の下にaddStar
やcreateProject
,updateProject
などの更新系のフィールドが生えている- このへんはただのRPCといえる
GraphQLのPros / Cons
主にRESTful APIとの比較。
Pros
- 特定のフィールド(たとえば
Blog#title
)のみリクエストして負荷を減らせる、というかリクエストするフィールドを常にすべて明示する設計- RESTful APIでも一部のフレームワーク(たとえば
the_garage
gem)はfields=
という同じことができる仕組みを持っていたりする
- RESTful APIでも一部のフレームワーク(たとえば
- GraphQLはスキーマが仕様に組み込まれている
- RESTful API にスキーマを当てる試みはいくつかあって、それはわりとうまくいったりする
- GraphQLのAPI console GraphiQL (グラフィクル)が優秀
- RESTful APIにスキーマをあてるフレームワークにも同様のAPI consoleがあったりはする
Cons
- HTTPにまつわる知見の多くが活用できない
- method, status code, cache, monitoring…
- 認証などは利用できる
- このへんはRPC over HTTPと全く同じ様相
- 新しすぎて情報が乏しい
- 技術的に枯れてない
- 静的型付き言語との相性がイマイチよくない
Railsアプリに組み込む
- GraphQL for Ruby: https://github.com/rmosolgo/graphql-ruby
- GraphiQL for Rails (API console): https://github.com/rmosolgo/graphiql-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数計算方法:
(https://developer.github.com/v4/guides/resource-limitations/ )
画像のアップロード
選択肢はいくつかあるので、どれを選ぶかはサービスの性質次第:
- base64 encodeしてvariablesに突っ込む
- MessgePackなどのbinaryを入れられるシリアライザでvariablesをシリアライズする
- さっき思いついたばかりなので未検証。実用している例は見当たらなかった。
- multipart/form-data でGraphQLとバイナリを同時に送る
- GraphQLを諦めてそこだけRESTful APIにする
Naming
snake_case
vscamelCase
- 小さいところとはいえ判断の迷うところ
- Rubyは
snake_case
- クライアントサイドの言語(JS, Java, Swift, …) は
camelCase
だから… content_html
をcamel case変換するとcontentHtml
とcontentHTML
の2パターンある問題 💢- Kibelaをどうするかはまだ決めてない
モニタリング
- エンドポイントごとのログがほぼ意味をなさなくなる
- 基本的にはツールチェインの成熟を待つしかない
- 自作するのであれば、GraphQLのASTを解析するなどが必要だが、技術的には可能
本日のまとめ
- GraphQLは「RESTful APIの一部のベストプラクティスをガチガチに強制するもの」と捉えると理解しやすい
- GraphQLそれ自体の話と現在の実装やツールチェインがどうかという話は分けて考えるべし
- Railsとの相性は悪くないが、ベストプラクティスはまだ定まらない感
- KibelaはWeb APIをGraphQLで公開します!