8717
1
0
GraphQL on Rails 2018, railsdm 2018/07/14
https://techplay.jp/event/679666
Topics
- GraphQL / graphql-ruby 2018 what's new
- GraphQL API の設計の勘所
- Analyzer API
- (Subscriptions)
- おまけ: webpack without webpacker
自己紹介
開発しているもの: Kibela
- https://kibe.la/
- コラボレーションソフトウェア
- a.k.a. 情報共有ツール
- 社内用の blog / wiki を提供するSaaS
- We are hiring!
Kibela Web API
- GraphQL製
- 内部APIとして
- 新規はGraphQLで実装
- 旧 RESTful API も徐々に GraphQL に移行中
- 外部APIとして
- 2018年中に公開予定
- 内部APIとschemaは同一にする予定
これまでの話
What's new in GraphQL / graphql-ruby
- GraphQL spec が "June 2018" にアップデートされた
- 前バージョンは "July 2015"
- 🆕 GraphQL subscriptions が仕様に入った
- 🆕 GraphQL errors.extensions が仕様に入った
- graphql-ruby が 1.8 にアップデートされた
- 🆕 class-based DSL になった
GraphQLとは
- GraphQLは Web API 用の仕様
- 必要最低限のリソースをリクエスト1発で取ってこれる
- SDD: Schema Driven Development のできる型安全なスキーマ
- Flux と相性がよい
- e.g. Flux的でない状態管理、たとえば upload / downloadのprogress indicatorの表示などは向かない
GraphQLを1年使ってわかったこと
- GraphQLの型設計は、RESTful APIのリソース設計よりもむしろ普通のOOPのクラス設計に近い
- N+1問題は、 RESTful API よりも制御しやすい
- 外部API として公開するにあたってのAPI制限は、 RESTful API よりも制御しやすい
GraphQLの型設計
- 用語
- 型(type) - GraphQLの構造単位
- フィールド(field) - 型は複数のフィールドからなる。引数も取れるのでメソッドにも見えるものもあるが、すべてフィールドと呼ぶ
- query - 狭義にはGraphQLの取得系の型。広義にはGraphQLのクエリ全般
定義例
- だいたい model (Item) に対応する GraphQL type (Types::ItemType) を定義する
- ↓のケースでは
Item
とUser
model が関係しているが、これらのmodelの型が直接現れていないことに注意- つまりダックタイピングが基本
ruby
class Types::ItemType < GraphQL::Schema::Object
description 'This is the Item type'
field :content, String, null: false
field :content_html, String, null: false, resolve: -> (item, _args, _context) {
MarkdownProcessor.render_to_html(item.content)
}
field :user, Types::UserType, null: false
field :likes, Types::UserType.connection_types
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
end
Field definition
ruby
field :content, String, null: false
-
content
field をnon-null String型として定義 - 素のGraphQL schemaと違い、
null
optionを必須とすることで「誤ってnullableになる」ことが避けられる- 本来のGraphQLは
T
が nullable,T!
がnon-null
- 本来のGraphQLは
- http://graphql-ruby.org/type_definitions/objects.html
Field definition: resolver
ruby
# field :content, String, null: false
# ↑は↓のショートカット
field :content, String, null: false, resolve: -> (item, _args, _context) {
item.content
}
# または
field :content, String, null: false, method: :content
- fieldごとにresolverによってオブジェクト(ここでは
item
)から値を取り出せる - デフォルトのresolverはフィールドの名前そのもの
設計の勘所
- GraphQLの型定義はスキーマであるとともにcontrollerでもあるので単純に保つ
- なるべくresolverを定義しない
- modelのassociationもそのままの構造にする
- has_manyは
Types::FooType.connection_type
にする(後述)
- has_manyは
- PORO model も活用する
Array vs Connection
- Arrayは素のJSON arrayに相当
- Connectionはページング可能なコレクション型で多機能
- 迷ったらConnection型にするべき
- PostgreSQLの array に関しては GraphQL の array にマッピングしてもよい
Connectionの構造
N+1問題
- RailsのRESTful APIは「controller#action ⇢ view」という順で実行する
- GraphQLは「frontendのview ⇢ GraphQL engine」という順で実行する
- RESTful APIにおけるN+1 問題は、「actionからはviewが何を求めるのか分からない」というのが問題を解決しにくくしている
- GraphQLは、「viewが必要なデータをクエリとする」⇢「GraphQL engineがそれを解釈してデータを収集(
resolve
)する - Rubyの場合、graphql-batch によって同じ型のフィールドをまとめて処理できる
- graphql-batch の適用はかなり手間がかかるが、一度適用すると view (query)をどのように変更しても動き続ける
- これにより、N+1問題は RESTful API よりも制御しやすいといえる
graphql-batchの使用法についてはこちら:
graphql-batch でバックエンドへのクエリを減らす - esm アジャイル事業部 開発者ブログ
API 制限問題
- RESTful APIにおける呼び出し制限も、「actionからはviewが何を求めるのか分からない」というのが問題を解決しにくくしている
- graphql-rubyの場合、
complexity
を GraphQL query AST から求められる- ただし Analayzer APIによってページング引数を必須にする必要あり
- GitHub API v4 はまさにページング引数を必須にしてcomplexityを静的に求めて制限をつけている
- フィールドごとにcomplexityを個別に設定できる(デフォルトは
1
)- つまり、markdown化やElasticsearchへのリクエストなどの重めのリクエストをそのように表現できる
- 外部API として公開するにあたってのAPI制限は、 RESTful API よりも制御しやすい
Analyzr API
-
query実行前 にASTをスキャンできる
- Analyzer APIを使ってconnectionフィールドのfirstまたはlast argumentを必須パラメータにする
- complexityの計算もAnalyzer APIによるもの
- http://graphql-ruby.org/queries/analysis.html
Subscriptions
- サーバーサイドからのイベント通知のためのspec
- transportは通常はWebSocketだが、仕様的にはなんでもいい
- コネクションも別でいいので、何なら Firebase Cloud Messaging などのサービス経由でもできるはず
- graphql-ruby は実装として ActionCable と Pusher (pub/sub as a service)
- graphql-ruby-client (JS) は LGPL 3.0 なので注意
Demo
https://github.com/gfx/graphql-subscriptions-demo
構成
- Rails 5.2
- unicorn
- graphql-ruby 1.8.x
- apollo-boost, react-apollo
- (webpack, webpack-serve, typescript, react, no-babel, no-webpacker)
感想
-
まだ早い
- 明らかに枯れてない感じの辛みが大量にある
- subscription queryのエラーが握りつぶされたり
- GraphiQL / rails console で試すのが難しかったり
- 明らかに枯れてない感じの辛みが大量にある
- subscription type の設計の仕方もわからないし、view (react-apollo) の作り方もよくわからない
- よくわからないので解説は省略。もうちょいこなれたらまたどこかで…
- ActionCableはまともに運用できる気がしないので、 AWS IoT で作ろうかなと構想中です
- AppSyncは使わないと思われる
付録: webpackerに依存しないrails+webpack構成
- 問題: webpackが生成したhash付きパスをrailsから参照したい
- webpackerは重厚すぎる
- e.g. KibelaではBabelを使っていないがwebpackerの依存にbabelがある
- webpack-rails gem は webpacker により deprecated に
- じゃあ webpack-rails を webpacker よりに再構成したらどうか
rails-webpack-manifests (仮)
- webpackの生成したpathを webpack-manifest-plugin でJSONとして参照できるようにする
webpack.config.js
const WebpackManifestPlugin = require("webpack-manifest-plugin");
const config = {
// ...
plugins: [
new WebpackManifestPlugin({
writeToFileEmit: production,
}),
// ...
]
};
// ...
manifest.json
{
"application.js": "/assets/application-d540c61ba189e711a3a1-development.js",
"application.js.map": "/assets/application-d540c61ba189e711a3a1-development.js.map"
}
- webpack_helper.rb でこの manifest.json を読んで変換
webpack_helper.rb
module WebpackHelper
WEBPACK_DEV_SERVER_PORT = 3808
# TODO: 本番の場合はCDNをホストとする
ASSET_HOST = "localhost:#{WEBPACK_DEV_SERVER_PORT}"
def webpack_path(name)
__webpack_manifest.fetch(name).yield_self do |path|
"//#{ASSET_HOST}#{path}"
end
end
def __webpack_manifest
# TODO: 本番の場合は public/assets/manifest.json をファイルシステムから読みこむ
@__webpack_manifest ||= JSON.parse(Net::HTTP.get('localhost', '/assets/manifest.json', WEBPACK_DEV_SERVER_PORT))
end
end
- 仕組み的には webpack-rails とまったく同じ
- manifest.json を生成する webpack plugin が違うだけ
- 依存は最低限
- webpack の知識がガッツリと必要