This post is Private. Only a writer or those who know its URL can access this post.

GraphQL「おい、今年もう終わるぞ」ぼく「まだあわてるような時間じゃない」

  • 0
    Comment
More than 1 year has passed since last update.

この記事はフィードフォースエンジニア Advent Calendar 2015 - Adventarの14日目の記事です。
13日目の記事は kasei_sanheroku+S3で遅延証明書をクローリングするアプリを作った話でした。


11891380_488616051306553_2117801174_n.jpg

発端

「あなた(RESTfulの事しか頭に無いサーバサイドエンジニア)は、わたし(jQuery から React はては Electron まで自在に操れる天才フロントエンドエンジニア)の気持ちなんて、なんにもわかってないのよ!!!!1!!」と、その手に持った GraphQL で百億万回しばかれた、あの社内勉強会から2ヶ月が過ぎようとしていますが、ぼくは今日も元気に RESTful してます。

さて、どうやら今年は12月までしかない様なので、重い腰を上げて GraphQL を知ったかぶりできる様になってから年を越したいと思います。

道具は揃ってる、気がする

件の勉強会にて、GitHub に Ruby 実装もあることを教えてもらいました。

今回は、こちらを利用して、Rails で実装したサンプルアプリに GraphQL を適用してみたいと思います。

できあがりはこちら

実は、GraphQL の Ruby 実装の方で、デモアプリも用意してくれています。

いっそ、これで終わりたいのですが、今回はこのデモアプリを教材として、ある程度 GraphQL を使える様になるために手を動かしてみたいと思います。

サンプルアプリ

シンプルなアプリがあれば良いと考え、適当なサンプルアプリとして古き良き掲示板(のバックエンド)を Rails を使ってでっちあげます。

え、Rails なんですか?

あ、はい。

Rails API

せっかくなので、API オンリーでいくために --api オプションを付けて諸々を切り捨てたアプリを作ります。

$ cat <<EOF > Gemfile
source 'https://rubygems.org'

gem 'rails', github: 'rails/rails'
gem 'rack',  github: 'rack/rack'
gem 'arel',  github: 'rails/arel'
EOF
$ bundle
$ rails new . --dev --force --api -T -d postgresql

「ダンコたる決意ってのができたよ」

思い切った名前を付けてみたい症候群

自分しか触らないコードを書く時って、そういうことありませんか?ぼくはあります。

  • 投稿する人: Contributor
  • 投稿する話題: Subject
  • 投稿したやつ: Contribution
db/schema.rb
ActiveRecord::Schema.define(version: 20151210145755) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"
  enable_extension "uuid-ossp"

  create_table "contributions", primary_key: "contribution_id", id: :uuid, default: "uuid_generate_v1()", force: :cascade do |t|
    t.uuid     "subject_id"
    t.uuid     "contributor_id"
    t.text     "content"
    t.datetime "created_at",     null: false
    t.datetime "updated_at",     null: false
  end

  create_table "contributors", primary_key: "contributor_id", id: :uuid, default: "uuid_generate_v1()", force: :cascade do |t|
    t.string   "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "subjects", primary_key: "subject_id", id: :uuid, default: "uuid_generate_v1()", force: :cascade do |t|
    t.uuid     "contributor_id"
    t.string   "title"
    t.datetime "created_at",     null: false
    t.datetime "updated_at",     null: false
  end

  add_foreign_key "contributions", "contributors", primary_key: "contributor_id", on_delete: :cascade
  add_foreign_key "contributions", "subjects", primary_key: "subject_id", on_delete: :cascade
  add_foreign_key "subjects", "contributors", primary_key: "contributor_id", on_delete: :cascade
end

勢いって怖いですね。気がついたら id という名前をやめてみたり、UUID を使ってみたりしてますよ。さりげなく on_delete: :cascade もしてますね。
ところで、外部キーのカスケードの負荷まわりに関するくわしい解説情報ってありませんかね?とても興味あります。

db/seeds.rb
ActiveRecord::Base.transaction do
  contributor = Contributor.create!(name: 'taro')
  subject = contributor.subjects.create!(title: 'Hi, GraphQL')
  subject.contributions.create!(contributor: contributor, content: 'TEST')
end

RESTful API

GraphQL が主役なので要りませんね。

GraphQL

サンプルアプリの準備も万端。早速 GraphQL しましょう。

ところで、GraphQL って?

SQLの様なクエリを投げ込んで結果を得るやつですね。ぼくはともかく、皆さんはご存じですよね。

ぼくは詳しく説明できないので、ご存じない方は上記をどうぞ。

スキーマを定義する

GraphQL であれこれするためにはスキーマを定義する必要があるので定義します。 ActiveRecord(ActiveModel) を上手く使えば省略できそうな気もしますが、今はいいでしょう。なんだかんだで、一筋縄ではいかなそうな気もしますし。

app/graph/schema.rb
ContributorType = GraphQL::ObjectType.define do
  name 'Contributor'
  description "A contributor"

  field :contributor_id, !types.ID
  field :name, !types.String
end

QueryType = GraphQL::ObjectType.define do
  name 'Query'
  description 'The query root of this schema'

  field :contributor do
    type ContributorType
    argument :contributor_id, !types.ID
    resolve -> (_, args, _) { Contributor.find(args['contributor_id']) }
  end
end

Schema = GraphQL::Schema.new(query: QueryType)

こうして、最低限のスキーマ定義を書くことで、クエリを実行する事ができる様になりました。以下は、IDを指定して投稿した人を特定するクエリを実行しています。

>> Schema.execute(<<EOF)
# To get specified contributor (this is comment text)
{
  contributor(contributor_id: "2236ad24-a003-11e5-916c-98e0d9aca763") {
    name
  }
}
EOF
  Contributor Load (0.7ms)  SELECT  "contributors".* FROM "contributors" WHERE "contributors"."contributor_id" = $1 LIMIT 1  [["contributor_id", "2236ad24-a003-11e5-916c-98e0d9aca763"]]
=> {"data"=>{"contributor"=>{"name"=>"taro"}}}

そうそう、GraphQL のクエリにはコメントだって書けるんですよ!

コントローラを書いて HTTP 越しに GraphQL する

ルーティングは適当に済ませます。

config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html

  get 'graphql', to: 'graph_ql#query'
end

先ほど定義したスキーマにクエリを与えて結果を JSON で返すだけのアクションです。

app/controllers/graph_ql_controller.rb
class GraphQlController < ApplicationController
  def query
    render json: Schema.execute(request.body.read)
  end
end

curl を使ってクエリを実行して結果を得てみましょう。

$ cat << QUERY | curl http://localhost:3000/graphql -s -X GET -d @- | jq .
{
  contributor(contributor_id: "2236ad24-a003-11e5-916c-98e0d9aca763") {
    name
  }
}
QUERY
{
  "data": {
    "contributor": {
      "name": "taro"
    }
  }
}

JSON Schema でスキーマを提供しておくと、クライアント側の GraphQL クエリの組み立てが捗るんでしょうか。よくわかりません。

mutation

query オペレーションだけでは読み込みしかできないので、mutation オペレーションも用意して書き込みもできる様にしてみましょう。

MutationType = GraphQL::ObjectType.define do
  name 'Mutation'
  description 'The mutation root of this schema'

  field :createContributor do
    type ContributorType
    argument :name, !types.String
    resolve -> (_, args, _) { Contributor.create!(name: args[:name]) }
  end
end

mutation オペレーションの型を定義してスキーマに与える様です。あまりよく分かってません。動けば良いのだ。

Schema = GraphQL::Schema.new(query: QueryType, mutation: MutationType)
$ cat << \QUERY | curl http://localhost:3000/graphql -s -X GET -d @- | jq .
mutation {
  createContributor(name: "Mutant") {
    contributor_id
  }
}
QUERY
{
  "data": {
    "createContributor": {
      "contributor_id": "256125ac-a0ac-11e5-8791-98e0d9aca763"
    }
  }
}
$ cat << \QUERY | curl http://localhost:3000/graphql -s -X GET -d @- | jq .
{
  contributor(contributor_id: "256125ac-a0ac-11e5-8791-98e0d9aca763") {
    name
  }
}
QUERY
{
  "data": {
    "contributor": {
      "name": "Mutant"
    }
  }
}

どこでどう管理するのが適切なのか、よくわかっていません。

完成!

「そんな GraphQL で大丈夫か?」
「大丈夫じゃない、問題だらけだ」

ちょっとだけ Working Draft を読んでおく

せっかくなので、Working Draft を斜め読みしながら、サンプルに手を加えてできる事を確認してみましょう。

Fragment

フラグメントが何を意味するのかよく分かりませんでしたが、共通部分を切り出して使い回せるんですね。

$ cat << QUERY | curl http://localhost:3000/graphql -s -X GET -d @- | jq .
query withFragment {
  contributor(contributor_id: "2236ad24-a003-11e5-916c-98e0d9aca763") {
    ...contributorFields
  }
}

fragment contributorFields on Contributor {
  contributor_id
  name
}
QUERY
{
  "data": {
    "contributor": {
      "contributor_id": "2236ad24-a003-11e5-916c-98e0d9aca763",
      "name": "taro"
    }
  }
}

...contributorFields の様に頭の ... は何かを省略してる訳では無く、そういう記法です。フラグメントに展開する、という意味ですね。

インラインなフラグメントがあったりインクルードできたりとか、色々な表現方法がある様ですね。使う場面はまだ想像できませんが。

Input Values

だいたい JSON です、かね?

  • Variable
  • Int
  • Float
  • String
  • Boolean
  • Enum
  • List
  • Object

Enum は微妙に想像が追いつきませんが、クォートしてない文字列表記(ex. MOBILE_WEB)なんだとか。

変数(Variable)を使ってクエリを再利用したりプレースホルダ的に使ったりとか、そんな感じなのかな。よくわかりません。

Directive

@include の様に @ から始まる形でディレクティブが定義されているのですね。

こんな感じですかね?わかりません。

cat << \QUERY | curl http://localhost:3000/graphql -s -X GET -d @- | jq .
query withFragment($condition: Boolean = true) {
  contributor(contributor_id: "2236ad24-a003-11e5-916c-98e0d9aca763") {
    ...contributorFields @include(if: $condition)
  }
}

fragment contributorFields on Contributor {
  contributor_id
  name
}
QUERY
{
  "data": {
    "contributor": {}
  }
}

クエリに与える変数によって、項目を含めるか否かを選択できるわけですね。

cat << \QUERY | curl http://localhost:3000/graphql -s -X GET -d @- | jq .
query withFragment($condition: Boolean = false) {
  contributor(contributor_id: "2236ad24-a003-11e5-916c-98e0d9aca763") {
    ...contributorFields @include(if: $condition)
  }
}

fragment contributorFields on Contributor {
  contributor_id
  name
}
QUERY
{
  "data": {
    "contributor": {
      "contributor_id": "2236ad24-a003-11e5-916c-98e0d9aca763",
      "name": "taro"
    }
  }
}

今の所、定義されているディレクティブは2つなんですかね?

  • @skip
  • @include

Type

型を見ると Eclipse ほしくなりますね!

  • Scalar (文字列とか整数みたいな primitive value)
    • Int
    • Float
    • String
    • Boolean
    • ID (ユニークな識別子)
  • Objects
  • Interfaces (抽象型)
  • Unions (抽象型)
  • Enums
  • Input Objects
  • Lists
  • Non-Null

Introspection

スキーマを見ることができたりするんですね。

cat << \QUERY | curl http://localhost:3000/graphql -s -X GET -d @- | jq .
{
  __type(name: "Contributor") {
    name
    fields {
      name
      type {
        name
      }
    }
  }
}
QUERY
{
  "data": {
    "__type": {
      "name": "Contributor",
      "fields": [
        {
          "name": "contributor_id",
          "type": {
            "name": "Non-Null"
          }
        },
        {
          "name": "name",
          "type": {
            "name": "Non-Null"
          }
        }
      ]
    }
  }
}

「今日のところは、このくらいでゆるしてやる!」

「もうやめて、ぼくのライフはゼロよ」もう、ぼくの気持ちはすっきりしました。安心して年を越すことができます。

「もう何も恐くない」

まとめ

rmosolgo/graphql-ruby のおかげで Ruby (Rails) でも手軽に簡易な GraphQL サービスを実装できる事が確認できました。
一方で、手間を減らす方法や、シンプルな実装に保ち保守性を担保する方法、賢い権限管理(Facebook の Permission 的なやつ)を実現する方法、mutation の正しい情報、等々、色々と見えないフリをしている事もたくさんあります。ミドルウェア機構あたりが気になっています。
ちゃんとした成果発表とするには、『gem ひとつ組み込んでルーティングでマウントするだけで Rails に GraphQL がにょきっと生えるやつ』を作って公開するくらいはやるべき、という声が聞こえてきますが鋼の精神をもつ不働明王たるぼくは気にしません。

要するに、「rmosolgo/graphql-ruby を全然読めてない(把握できてない)し、プロダクトに組み込める程の知見はまだない、でもみんなが幸せになる未来が開けたらいいですね!」という事をここに宣言してぼくの今年を締めくくりたいと思います。

おまけ:作成したサンプルはこちら


明日の担当は G の刺客 a-know です。