この記事はフィードフォースエンジニア Advent Calendar 2015 - Adventarの14日目の記事です。
13日目の記事は kasei_san のheroku+S3で遅延証明書をクローリングするアプリを作った話でした。
発端
「あなた(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
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
もしてますね。
ところで、外部キーのカスケードの負荷まわりに関するくわしい解説情報ってありませんかね?とても興味あります。
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
) を上手く使えば省略できそうな気もしますが、今はいいでしょう。なんだかんだで、一筋縄ではいかなそうな気もしますし。
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 する
ルーティングは適当に済ませます。
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 で返すだけのアクションです。
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 です。