bitjourney team
8631
1
0

GraphQL on Rails 2018, railsdm 2018/07/14

Published at July 14, 2018 4:33 p.m.
Edited at July 14, 2018 6:15 p.m.

https://techplay.jp/event/679666

Topics

  • GraphQL / graphql-ruby 2018 what's new
  • GraphQL API の設計の勘所
  • Analyzer API
  • (Subscriptions)
  • おまけ: webpack without webpacker

自己紹介

開発しているもの: Kibela

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) を定義する
  • ↓のケースでは ItemUser 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
  • 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 にする(後述)
  • 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

Subscriptions

  • サーバーサイドからのイベント通知のためのspec
  • transportは通常はWebSocketだが、仕様的にはなんでもいい
    • コネクションも別でいいので、何なら Firebase Cloud Messaging などのサービス経由でもできるはず
  • graphql-ruby は実装として ActionCablePusher (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 の知識がガッツリと必要

rails-wepback-manifest (仮)の見どころ

We are hiring!

エンジニア採用やってます!(We're Hiring Engineers!) - Kibela Blog