Edited at

[2018年最新版]RubyonRailsを使ってGraphQLを学んでみる


はじめに

この記事は、現在実務でGraphQLを使っている筆者が、Inputで得た知見を自分なりにまとめたものです。

公開してはいますが、日々キャッチアップしたら都度更新していくので、優しく見守っていただけると!

記載している文章内で不備などありましたら、ご指摘いただけると幸いです。


対象となる読者


  • Class_BaseにGraphQLの書き方を知りたい人

  • GraphQLでのMutationの書き方がいまいちわからない人

  • とりあえず、RailsとGraphQLでCRUDをやってみたい人


前提

GraphQLってなに?などの概念的な話はあまり出てきません。

それらについては、他のサイトや記事でたくさん紹介されているので、参考リンクを参照ください。


参考リンク

https://qiita.com/syu_chan_1005/items/3350f1d12c17a77e98c7

https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad

https://www.webprofessional.jp/rest-2-0-graphql/


GraphQlとRESTの違い

GraphQl
REST

HTTP上などで動作する
HTTP上などで動作する

エンドポイントが一つだけ
エンドポイントが複数ある

1リクエストで多くのリソースを包括
1リクエストにつき1リソース

フェッチしない
フェッチが過剰


Gemfileにgraphqlを追加

gem 'graphql'


GraphQLをGenerateする

rails g graphql:install

次のようにファイル群が作成される。


Running via Spring preloader in process 75061
create app/graphql/types
create app/graphql/types/.keep
create app/graphql/bookshelf_schema.rb
create app/graphql/types/base_object.rb
create app/graphql/types/base_enum.rb
create app/graphql/types/base_input_object.rb
create app/graphql/types/base_interface.rb
create app/graphql/types/base_union.rb
create app/graphql/types/query_type.rb
add_root_type query
create app/graphql/mutations
create app/graphql/mutations/.keep
create app/graphql/types/mutation_type.rb
add_root_type mutation
create app/controllers/graphql_controller.rb
route post "/graphql", to: "graphql#execute"
Skipped graphiql, as this rails project is API only
You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app


GraphiQLをbrewでinstall

公式ページ: https://electronjs.org/apps/graphiql

brewを使って、MacにGraphiQLをインストールする

brew cask install graphiql


GraphiQLでクエリを投げてみる

GraphQL Endpointに以下のURLを入力する

http://api.graphloc.com/graphql

次に、緯度と経度のクエリを投げて、動作確認してみる

{

getLocation(ip: "8.8.8.8") {
location {
latitude
longitude
}
}
}

成功すると、次のようなデータが返ってくる。

{

"data": {
"getLocation": {
"location": {
"latitude": "37.751",
"longitude": "-97.822"
}
}
}
}


GrahiQLの操作

Rails serverでアプリケーションサーバーを起動させていれば、GraphiQLでも動作させることができる。

スクリーンショット 2018-09-15 23.20.27.png

正しく起動できていれば、< Docsが開けるようになるので、クリックするとDocumentation Explorerが開く。

スクリーンショット 2018-09-15 23.23.38.png

Queryをクリックすると、現在プロジェクト内のquery_type.rbで定義されているクエリが確認できる。

JSONについては、以下のWikipediaを参照のこと。

https://en.wikipedia.org/wiki/JSON


query_typeの書き方

rails g graphql:installを実行すると、以下のようなquery_type.rbが生成される

module Types

class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.

# TODO: remove me
field :test_field, String, null: false,
description: "An example field added by the generator XYZ"
def test_field
"Hello World!"
end
end

表示されている項目を説明すると、

field
:test_field
String
null: false
description

fieldを定義
DBでいうところのカラム名
fieldのデータ型
nullを許可するかどうか
fieldの説明

こんな感じ。


filedに引数を定義したい場合

書き方としては、ブロックを作り、そこにargumentとして引数を定義。

定義する際には、データ型と必須であればrequired: trueを記載する。

あとは、メソッドの引数に通常通り与えてあげればOK。

module Types

class QueryType < Types::BaseObject
field :test_field, String, null: false,
description: "An example field added by the generator XYZ"do
argument :name, String, required: true
end
# フロント側で呼び出すfieldをメソッド形式を定義
def test_field(name:)
"Hello #{name}"
end
end
end

GraphiQLをリロードして確認してみると、引数にnameが追加されているのがわかる。

スクリーンショット 2018-09-15 23.54.43.png

testFieldの引数にnameを指定して、適当な文字列を追加してあげると、ちゃんと定義通りに反映されているのがわかる。

スクリーンショット 2018-09-15 23.56.20.png


Contextについて

contextは各クエリ固有の情報を、後述するresolveメソッドにhash形式で渡す役割を持っている。

graphql_controller.rbを開くと、次のようなコードになっているのがわかります。

class GraphqlController < ApplicationController

def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
time: Time.now
# Query context goes here, for example:
# current_user: current_user,
}
result = BookshelfSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end

private

# 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

def handle_error_in_development(e)
logger.error e.message
logger.error e.backtrace.join("\n")

render json: { error: { message: e.message, backtrace: e.backtrace }, data: {} }, status: 500
end
end

この中にcontext = {}という箇所があり、そこにresolveメソッドに渡したいcontextを定義します。

(省略)

context = {
time: Time.now
}
(省略)

続いて、query_type.rbでcontext[:time]`を渡してあげましょう。

(省略)

def test_field(name:)
Rails.logger.info context[:time]
"Hello #{name}!"
end
(省略)

この状態でGraphiQLでクエリを投げ、Railsのログを見てみると、

Started POST "/graphql" for 127.0.0.1 at 2018-09-16 00:13:18 +0900

Processing by GraphqlController#execute as HTML
Parameters: {"query"=>"{\n testField(name: \"Ruby\")\n}", "variables"=>nil, "graphql"=>{"query"=>"{\n testField(name: \"Ruby\")\n}", "variables"=>nil}}
2018-09-16 00:13:18 +0900 #=> context = { time: Time.now }で定義した部分
Completed 200 OK in 43ms (Views: 0.7ms | ActiveRecord: 0.0ms)

現在の時刻が表示されているのがわかります。

使い所としては、例えば、現在ログインしているのが誰なのかを表すcurrent_userなどを渡せば、ログで確認することができるということですね。


Scalarについて

スカラーは、GraphQLで定義したオブジェクトごとのキーと値を独自のデータ型で管理したもの。

GraphQLでデフォルトで用意されているスカラー型は、以下の5つ。

String
Int
Boolean
Id
Float

UTF-8のシーケンス
符号付き32ビットの整数
true or false
オブジェクトごとに割り振られたID
符号付き倍精度浮動小数点値

ただ、スカラー型については独自に定義することができるので、これを応用すればカスタマイズすることが可能。


Null制約について

GraphQLでは、それぞれのfieldごとにnull制約をつけることができる。

null; falseを設定したfieldは、フロント側でクエリを取得した時やmutationを実行した際にエラーが返るようになる。

スクリーンショット 2018-09-16 11.23.29.png

サーバーサイド側でもnull制約を設けたい場合は、別途バリデーション定義をすればOK。


独自にQuery_typeを定義する

ActiveRecordを使って、独自のQuery_typewを定義することが可能。

例えば、full_nameというメソッドを独自に定義したい場合は、該当するモデルに定義し、query_typeにそのfieldを追記してあげればいい。

# author.rb

class Author < ApplicationRecord
def full_name
([first_name, last_name].compact).join " "
end
end

# author_type.rb

class Types::AuthorType < Types::BaseObject
(省略)
field :full_name, String, null: false, camelize: false
end

ちなみに、モデルにロジックを書かなくても、各query_type.rbに記載しても同じ結果が得られる。

その場合は、定義するobjectのfield名のprefixにobjectをつけてあげればOK。

(GraphQL側でObjectの対象となるモデル名を検知してくれる)

# author_type.rb

class Types::AuthorType < Types::BaseObject
(省略)
field :full_name, String, null: false, camelize: false

def full_name
([object.first_name, object.last_name]).compact).join " "
end
end

GraphiQLで実際に叩いてみると、定義されたfieldが正常にresponseされるのがわかる。

スクリーンショット 2018-09-16 12.02.19.png


fieldTypeをカスタマイズする

GraphQLでは、Fieldtypeも独自定義することが可能。

やり方は簡単で、


  1. モデルに独自定義したいfieldTypeをメソッドとして定義する


  2. graphql/typesディレクトリ配下に新規で1と同じ名前のquery_type.rbを作成

  3. 取得するためのfieldを定義する

  4. モデルに対応した各query_type.rbにfieldを追記する


モデルに独自定義したいfieldTypeをメソッドとして定義する

例えば、緯度と経度を作成するcoordinatesというFieldTypeをメソッドとしてモデルファイルに定義する。

class Author < ApplicationRecord

def coordinates
# 緯度と経度について、それぞれ定義するメソッド
# 第一要素が緯度で第二要素が経度
[rand(90), rand(90)]
end
end


graphql/typesディレクトリ配下に新規で1と同じ名前のquery_type.rbを作成

touch app/graphql/types/coordinates_type.rb


ルートレベルで取得するためのfieldを定義する

# coordinates_type.rb

class Types::CoordinatesType < Types::BaseObject
field :latitude, Float, null: false
field :longitude, Float, null: false

def latitude
object.first
end

def longitude
object.last
end
end


モデルに対応した各query_type.rbにfieldを追記する

# author_type.rb

class Types::AuthorType < Types::BaseObject
field :coordinates, Types::CoordinatesType, null: false
end


QueryTypeに配列を定義する

配列を定義するにはまず、データベースの玄関口であるモデルに対象となるfieldをメソッドで定義する。

# author.rb

class Author < ApplicationRecord
def publication_years
# 出版した年度が配列でランダムに最大10個返ってくる
(1..rand(10)).to_a.map { 1900 - rand(100)}
end
end

次に、各query_typeを定義しているファイルに、fieldを定義する。

class Types::AuthorType < Types::BaseObject

field :publication_years, [Int], null: true
end

最後に、ルートレベルのquery_type.rbにfieldとArrayに対応した呼び出しメソッドを定義する。

field :authors, [Types::AuthorType], null: false

def authors
Author.all
end

これでGraphiQLを叩くと、複数形にすればArrayでQueryが取得できる。

スクリーンショット 2018-09-16 14.21.00.png


MutaionによるCRUD処理

Mutationはデータの変更を行うときに使うfield。

Create,Update, Deleteのような既存のfieldを変更する際に使用される。


MutationによるCreate

MutationでCreateを行う方法は2通りある。


その1:typesディレクトリ内にあるmutation_type.rbに記述する

# app/graphql/types/mutation_type.rb

module Types
class MutationType < Types::BaseObject

field :create_author, AuthorType, null: true, description: "Create an author" do
argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
end

def create_author(first_name:, last_name:, yob:, is_alive:)
Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end

end
end

create処理をするfieldを用意して、引数に何を与えるかの処理をブロックで記述。

 field :create_author, AuthorType, null: true, description: "Create an author" do

argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
end

その後、ActiveRecordによるデータベースに問い合わせるための処理をメソッドの形式で定義する。

 def create_author(first_name:, last_name:, yob:, is_alive:)

Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end


その2:mutationsディレクトリにmutation.rbを作成して記述する

class Mutations::CreateAuthor < GraphQL::Schema::Mutation

null true

argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false

def resolve(first_name:, last_name:, yob:, is_alive:)
Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end
end

mutationsディレクトリにmutation.rbを作成し、createする際に必要な引数を定義する

  argument :first_name, String, required: false, camelize: false

argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false

その後、resolveメソッドを定義し、引数に必要なもfieldを渡し、ActiveRecordでDBへ問い合わせるための処理を記述する。

  def resolve(first_name:, last_name:, yob:, is_alive:)

Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end

最後に、typesディレクトリ内にあるmutation_type.rbで定義したmutationを呼び出してあげれば、その1と挙動が同じになる。

module Types

class MutationType < Types::BaseObject
field :create_author, Types::AuthorType, mutation: Mutations::CreateAuthor
end
end


Query_Variableについて

スクリーンショット 2018-09-16 16.45.56.png

GraphiQLでMutationを実行するとき、query_variableを使うと、fieldの値を別ウィンドウで定義できるので便利。

mutation createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean){

createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive) {
id
full_name
}
}

mutationに続いてMutation名を記述し、field名のprefixに$をつけてあげる。



createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean)

続いて、mutationの箇所には$のついたfield名を渡してあげる。

createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive)

あとは、GraphiQLのQUERY VARIABLESの箇所にJSON形式でfield名に対応した渡したい値を記述してあげればOK。

{

"first_name": "Yammy",
"last_name": "Humberg",
"yob": 2019,
"is_alive": true
}

成功すると、次のようなresponseになる。



{
"data": {
"createAuthor": {
"id": "7",
"full_name": "Yammy Humberg"
}
}
}


InputTypesについて

これまでCreateを行うときには、GraphiQLで次のように書いてきました。

mutation createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean){

createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive) {
id
full_name
}
}

ただ、これだと同じfield名が2回も登場していて、全然DRYではないですよね。

そこで登場するのが、InputTypeです。

InpuTypeは、GraphQL::Schema::InputObjectを継承させて次のように定義します。

class Types::AuthorInputType < GraphQL::Schema::InputObject

graphql_name "AuthorInputType"
description "All the attributes for creating an author"

argument :id, ID, required: false
argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
end

InputTypeの各項目については、次の通りです。

argument
required
graphqql_name
description

InputTypeに必要な引数
必須かどうか
InputType名
InputTypeの説明

次に、MutationTypeで定義したCreateをInputTypeで書き換えます。

class Types::MutationType < Types::BaseObject

field :create_author, AuthorType, null: true, description: "Create an author" do
argument :author, Types::AuthorInputType, required: true
end
def create_author(author:)
Author.create author.to_h
end
end

あとはGraphiQLで以下のようにクエリを投げ、QUERY VARIABLESで値を指定すればOK。

スクリーンショット 2018-09-17 12.49.15.png


QUERY



mutation createAuthor($author:AuthorInputType!) {
createAuthor(author:$author) {
id
full_name
}
}


QUERY VARIABLES

{ "author": {

"first_name": "Hina",
"last_name": "Okamoto",
"yob": 1933,
"is_alive": true
}
}


MutationによるUpdate

スクリーンショット 2018-09-18 09.21.58.png

InputTypeまでできたら、あとは簡単。個別にUpdateのMutationを作成してあげればいいんです。

class Types::MutationType < Types::BaseObject

field :update_author, Boolean, null: false, description: "Update an author" do
argument :author, Types::AuthorInputType, required: true
end

def update_author(author:)
existing = Author.where(id: author[:id]).first
#&.でnil以外なら更新されたデータのhashを返す
existing&.update author.to_h
end

ここで、update_authorBooleanの指定があることに、疑問を持った方がいるかもしれません。

これは、フロント側はデーターベース側にどんな変更があったかを知る必要がないので、基本的にApplicationRecordの戻り値を渡さなくてもOKだから。

そのため、フロント側ではBoolean型にしてupdateがtrueなのかfalseなのかを返せばいいので、このようになっています。


MutationによるDelete

スクリーンショット 2018-09-18 09.28.06.png

deleteではInputTypeを使わず、削除対象となるIDを渡してあげればOK。

class Types::MutationType < Types::BaseObject

field :delete_author, Boolean, null: false, description: "Delete an author"do
argument :id, ID, required: true
end

def delete_author(id:)
Author.where(id: id).destroy_all
true
end
end

destroy_allによって、ActiveRecordを介してDBにIDを問い合わせ、レコードを削除しています。