この記事はSmartDrive Advent Calendar 2017の4日目の記事です。
こんにちは、SmartDriveでバックエンドエンジニアをしている@ryota-koganezawa です。これからローンチ予定のプロダクトに導入したGraphQL(ruby)と各種プラグインついてお話しします。
また、できるだけ簡潔に記述することを目的にしています。詳しいことを知りたい方は他の記事を参考にしてください。

主に下記について触れていきたいと思います。

  • GraphQLとは?
  • GraphQLを採用した理由
  • GraphQL-rubyのCacheにおけるTips
  • GraphQL-rubyのN+1対応
  • GraphQLのAPIドキュメント
  • インターフェイス
  • 感想
  • おわりに

GraphQLとは?

  • 2012年にFacebookが開発、2015年7月にRFCドラフト案を公開
  • クライアント・サーバアプリケーション向けのデータモデリング記述のためのクエリ言語
  • 詳細はこちらをご覧ください。

GraphQLを採用した理由

GraphQLを導入するメリットとしてよくあげられるのもの

  • 階層構造を備えたクエリ構造
  • ビュー要求の記述しやすさ
  • 強力な型システム...etc

等があると思いますが、私が導入を決めたのは別の理由からでした。
まず、私が置かれている状況としては...

  • 新規サービス開発において、モデリングだけがFixした
  • フロント実装(デザイン含め)がなかなか定まらない。且つ変更される可能性大
    • ステークホルダーが多く、仕様が局所的にFixされていく。
    • つまり、バックエンド側は柔軟に設計し、それなりの変更要求に耐えられる実装にしなくてはならない

柔軟な設計というと、抽象度をあげ単一責任の原則を守り...依存関係逆転の原則 ...インターフェース分離の原則...うんぬんかんぬん...と各種プラクティスはあると思うのですが、『モデリングだけがある程度固まっている』この状況でバックエンドを進捗させていく、適した設計パターンが思いつきませんでした。もしくは、全てのリソースに対応したRESTful APIのエンドポイントを用意するか...

この時、運命的に出会ったのがGraphqlです。階層構造化されたモデル郡に対応させたエンドポイントを用意し『後は好きに使ってね』という感じにして、こちらのタスクについては気にすることなく、別のタスク・案件・チームビルド等に注力できるのです。バックエンドのみですが、実際に導入にかかったコスト(学習コスト含め)1日〜2日程度。
また、queryとmutationを同一のエンドポイント(同一メソッド)で受けるのはさすがに気が引けたので、今回取り入れたのはqueryのみです。

GraphQL-rubyのCacheにおけるTips

Cache関連についてはgithub staff Marc-Andréさんの記事を参考にさせて頂きました。
queryのparseに多少のコスト(クエリの大きさにもよる)がかかるので、そこをcacheしましょうという内容です。
参考にした記事にあるコードを多少書き換えていますが、下記サンプルコードです。

variables = ensure_hash(params[:variables]
result = Schema.execute(document: document, variables: variables)

# Caching GraphQL queries with GraphQL-ruby and Rails.
# @see: github staff(Marc-André) http://mgiroux.me/2016/graphql-query-caching-with-rails/
def document
  ::Rails.cache.fetch(cache_key, expires_in: EXPIRES_IN) do
    GraphQL.parse(params[:query])
  end
end

# Handle form data, JSON body, or a blank value.
def ensure_hash(ambiguous_param)
  case ambiguous_param
  when String
    if ambiguous_param.present?
      ensure_hash(JSON.parse(ambiguous_param))
    else
      {}
    end
  when Hash, ActionController::Parameters
    ambiguous_param
  when nil
    {}
  else
    raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
  end
end

GraphQL-rubyのN+1対応

GraphQL導入にあたり懸念されるものとして、N+1があります。
こちらの解決方法として導入したのが、graphql-batchです。

README.mdには1つのローダーしか記載されていませんが、実際には2パターン用意されています。ともにカスタマイズ可能。それらexamplesは下記になります。

  • 初期化時にモデルを渡し、perform時にWhere句の条件を渡すパターン
  • 初期化時にモデルを渡し、perform時にassociationを渡すパターン

GraphQLのAPIドキュメント

api documentとしては、GraphiQL一択かなと思います。
こちらは、デバック用途としても利用でき且つapiドキュメントサーバーになる優れものです。GraphiQL IDEとして紹介されていたりもします。

GraphiQLのREADME.mdより抜粋
graphiql.png

README.mdのとおりにgraphiqlをmountし、http://localhost:8080/graphiqlへアクセスするとqueryのデバック実行用のapi consoleが表示されます。
schema定義については、api console立ち上げ時にapp/graphql/types配下にある各種typeを読み込みfieldsの情報等を表示してるようです。
このurlをフロントエンド担当者へ伝えれば、schema定義を参照しデバックしながら開発することができます。

インターフェイス

よりDRYにRailsっぽく記述する方法としてインターフェース(GraphQL::InterfaceType)が用意されています。定義したインターフェースをGraphQL::ObjectTypeで継承すれば、同じフィールドを何個も定義する必要がなくなります。

実装サンプル

::Types::ActiveRecordInterface = GraphQL::InterfaceType.define do
  name 'ActiveRecordInterface'
  description 'ActiveRecordInterface'

  field :id, !types.Int
  field :updated_at, !types.String
  field :created_at, !types.String
end
::Types::HogeType = GraphQL::ObjectType.define do
  interfaces [::Types::ActiveRecordInterface] # ここで共通フィールドとして読み込む

  name 'Hoge'
  description 'Hoge'

  field :code, !types.String
  field :name, !types.String
end

GraphQL::InterfaceTypeについては、こちらの記事がとても参考になるかと思います。

感想

学習コストも低く用意されているプラグイン等も豊富にあるので、実際導入にはそれほど苦労はありませんでした。これからフロントエンド側の開発が始まるので、フロントエンド側のTipsも纏めチーム内でGraphQLを評価してみたいと思います。

おわりに

SmartDriveではバックエンドエンジニアの採用活動を積極的に行っております。
また、その他各種エンジニアも絶賛募集中です。ご興味がありましたら是非ご応募下さい!!