GraphQLを試してみた2 -RubyでGraphQLを試してみた-

本稿はRubyでGraphQLのバックエンド側の処理を書いた際(というか調べた際)のメモ。
基本的にググったり、[公式ページ](https://graphql-ruby.org/)を見たりした。
(結果から言うと、公式の情報だけで十分だった気もする……)

尚、本稿の前提は以下の通り。

* Rubyが分かる。
* Bundlerをインストール済み (なくても意味は分かれば十分)。
* GraphQLが何者を知ってる。

# 本稿の各サンプルの実行方法

最初に本稿の各サンプルコードの実行方法を示す。

* 以下のようなGemfileを用意する。

```:Gemfile
source "https://rubygems.org"

gem "graphql"
```
* GraphQLライブラリをインストールする。

```shell-session
$ bundle install --path vendor/bundle
```

* サンプルコードを保存し、以下のように実行する。

```shell-session
$ bundle exec ruby xxxx.rb
```

ファイルの保存が面倒であれば以下のようにirb経由でも良い。

```shell-session
$ bundle exec irb
```

# Query

GraphQLライブラリでQueryを書く方法は以下の2つ。

* defineメソッドを使用する方法
* 子クラスを定義する方法

## defineメソッドを定義する方法

```ruby:test1.rb
require 'graphql'

QueryType = GraphQL::ObjectType.define do
  name 'Query'
  field :name do
    type types.String
    description 'the name of this thing'
    resolve -> (obj, args, ctx) { 'Nanashi' }
  end
end

Schema = GraphQL::Schema.define do
  query QueryType
end

puts Schema.execute('{name}').to_json
puts Schema.execute('{n1:name, n2:name}').to_json
```

```shell-session:実行結果
$ bundle exec ruby test1.rb 
{"data":{"name":"Nanashi"}}
{"data":{"n1":"Nanashi","n2":"Nanashi"}}
$
```

## 子クラスを定義する方法

Nullableの設定有無などの差異はあるが、以下は基本的にtest1.rbと同じような内容である。

```ruby:test2.rb
require 'graphql'

class QueryType < GraphQL::Schema::Object
  description 'query'
  field :name, String, null:true, description:'the name of this thing'
  def name()
    'Nanashi'
  end
end

class Schema < GraphQL::Schema
  query QueryType
end

puts Schema.execute('{name}').to_json
puts Schema.execute('{n1:name, n2:name}').to_json
```

実行結果はtest1.rbと完全に同じである。

---

## Queryを使ったサンプル

### 動的にクエリ結果を変更する例

```ruby:test3.rb
require 'graphql'

$id = 0

class QueryType < GraphQL::Schema::Object
  field :id, ID, null:true, description:'id'
  field :name, String, null:true, description:'name'
  def id()
    retval = $id
    $id += 1
    return retval
  end
  def name()
    'Nanashi'
  end
end

class Schema < GraphQL::Schema
  query QueryType
end

puts Schema.execute('{id}').to_json
puts Schema.execute('{id,name}').to_json
puts Schema.execute('{id1:id, id2:id}').to_json
```
```shell-session:実行結果
$ bundle exec ruby test3.rb
{"data":{"id":"0"}}
{"data":{"id":"1","name":"Nanashi"}}
{"data":{"id1":"2","id2":"3"}}
$
```

### 引数ありクエリ

```ruby:test4.rb
require 'graphql'

class QueryType < GraphQL::Schema::Object
  field :func, String, null:false do
    argument :id, Int, required: true
    argument :sex, String, required: false
    description 'the name of this thing'
  end
  def func(id:, sex:'male')
    "No. #{id}_sex:#{sex}"
  end
end

class Schema < GraphQL::Schema
  query QueryType
end

puts Schema.execute('{func(id:123)}').to_json
puts Schema.execute('{func(id:123, sex:"female")}').to_json
```

```shell-session:実行結果
$ bundle exec ruby test4.rb
{"data":{"func":"No. 123_sex:male"}}
{"data":{"func":"No. 123_sex:female"}}
$
```

### 型定義

型定義については本稿では省略する。
興味がある場合は公式ページの下記を参照。

https://graphql-ruby.org/type_definitions/objects.html

# Mutation

Rubyでは、Mutationを以下のように記載する。

```ruby:test5.rb
require 'graphql'

class UpdateAddressMutation < GraphQL::Schema::Mutation
  null true

  argument :pcode, Int, required: false
  argument :address, String, required: false

  field :rc, String, null:false
  field :output, Int, null:false

  def resolve (pcode: nil, address: nil)
    str="pcode:#{pcode}, address:#{address}"
    puts str
    { rc:'OK', output:str }
  end
end

class MutationType < GraphQL::Schema::Object
  field :updateAddress, mutation: UpdateAddressMutation
end

class Schema < GraphQL::Schema
  mutation MutationType
end

puts Schema.execute('mutation { updateAddress(pcode:123456, address:"Fuga-shi, Hoge-ken"){ rc } }').to_json
```

```shell-session:実行結果
$ bundle exec test6.rb
pcode:123456, address:Fuga-shi, Hoge-ken
{"data":{"updateAddress":{"rc":"OK"}}}
$
```

## Relay形式のMutation

Relayについての詳細は割愛するが、Relay準拠でMutationを使用する場合、
「GraphQL::Relay::Mutation」を使用する。

```ruby:test6.rb
require 'graphql'

UpdateAddressMutation = GraphQL::Relay::Mutation.define do
  name 'UpdateAddress'
  description 'example'

  input_field :postal_code, !types.Int
  input_field :address, !types.String

  return_field :rc, !types.Int
  return_field :v, !types.String

  resolve ->(_obj, inputs, ctx) {
    { rc:200, v:"No.#{inputs[:postal_code]}"}
  }
end

class MutationType < GraphQL::Schema::Object
  field :updateAddress, field: UpdateAddressMutation.field
end

class Schema < GraphQL::Schema
  mutation MutationType
end

puts Schema.execute('mutation { updateAddress(input :{postal_code:123, address:"Hoge-Ken"}){ v } }').to_json
puts Schema.execute('mutation { updateAddress(input :{postal_code:123, address:"Hoge-Ken"}){ rc, v } }').to_json
puts Schema.execute('mutation { v1:updateAddress(input :{postal_code:123, address:"Hoge-Ken"}){ v }, v2:updateAddress(input :{postal_code:345, address:"Fuga-Ken"}){ rc } }').to_json
```

```shell-session:実行結果
$ bundle exec ruby test6.rb
{"data":{"updateAddress":{"v":"No.123"}}}
{"data":{"updateAddress":{"rc":200,"v":"No.123"}}}
{"data":{"v1":{"v":"No.123"},"v2":{"rc":200}}}
$
```

詳細は以下を参照。

https://graphql-ruby.org/relay/mutations.html

ググった感じでは、Ruby+GraphQLの日本語記事は大体Relay形式だった。

# その他

## エラーを返却

エラーを返却する場合は以下のように例外を送出する。
以下はtest5.rbのresolve定義を変更したものである。

```ruby:test7.rb
require 'graphql'

class UpdateAddressMutation < GraphQL::Schema::Mutation
  null true

  argument :pcode, Int, required: false
  argument :address, String, required: false

  field :rc, String, null:false
  field :output, Int, null:false

  def resolve(pcode: nil, address: nil)
    raise GraphQL::ExecutionError.new('Oh!')
  end
end

class MutationType < GraphQL::Schema::Object
  field :updateAddress, mutation: UpdateAddressMutation
end

class Schema < GraphQL::Schema
  mutation MutationType
end

puts Schema.execute('mutation { updateAddress(pcode:123456, address:"Fuga-shi, Hoge-ken"){ rc } }').to_json
```

```shell-session:実行結果
{"data":{"updateAddress":null},"errors":[{"message":"Oh!","locations":[{"line":1,"column":12}],"path":["updateAddress"]}]}
```

## Sinatra + GraphQL

Sinatra+GraphQLを使ったサンプルは以下の通りである。

```:Gemfile
source "https://rubygems.org"

gem "sinatra"
gem "sinatra-contrib"
gem 'rack-contrib'
gem "graphql"
```

```ruby:sinatra_graphql.rb
require 'graphql'
require 'sinatra'
require 'sinatra/json'
require 'rack/contrib'
use Rack::PostBodyContentTypeParser

$list = {}
$id = 0

def reqId()
  rc = $id
  $id += 1
  return rc
end

class QueryType < GraphQL::Schema::Object
  field :user, String, null:true do
    argument :id, Int, required:false
  end
  def user(id:)
    $list[id]
  end
end

class CreateMutation < GraphQL::Schema::Mutation
  argument :name, String, required: true

  field :id, Int, null:false
  field :rc, String, null:false

  def resolve (name:)
    i = reqId()
    $list[i] = name
    { rc:'OK', id:i }
  end
end

class MutationType < GraphQL::Schema::Object
  field :create, mutation: CreateMutation
end

class Schema < GraphQL::Schema
  query QueryType
  mutation MutationType
end

post '/graphql' do
  json Schema.execute(params[:query], variables: params[:variables]).to_json
end
```

* サーバ側でアプリを実行する。

```shell-session:サーバ側_実行開始
$ bundle exec ruby sinatra_graphql.rb
[2019-05-20 18:23:08] INFO  WEBrick 1.3.1
[2019-05-20 18:23:08] INFO  ruby 2.3.3 (2016-11-21) [x86_64-linux-gnu]
== Sinatra (v2.0.5) has taken the stage on 4567 for development with backup from WEBrick
[2019-05-20 18:23:08] INFO  WEBrick::HTTPServer#start: pid=1361 port=4567
```

* ID=0のユーザを問い合わせる(ユーザなし)。

```shell-session:実行結果1
$ curl -H 'Content-type: application/json' -X POST http://localhost:4567/graphql -d '{ "query" : "query { user(id:0)}" }'
"{\"data\":{\"user\":null}}"
```

* ユーザを登録する。

```shell-session:実行結果2
$ curl -H 'Content-type: applicatalhost:4567/graphql -d '{ "query" : "mutation { create(name:'Ken'){id} }" }'
"{\"data\":{\"create\":{\"id\":0}}}"
```

* ID=0のユーザを問い合わせる(ユーザあり)。

```shell-session:実行結果3
$ curl -H 'Content-type: application/json' -X POST http://localhost:4567/graphql -d '{ "query" : "query { user(id:0)}" }'
"{\"data\":{\"user\":\"Ken\"}}"
```

* 2ユーザをまとめて登録する。

```shell-session:実行結果4
$ curl -H 'Content-type: application/json' -X POST http://localhost:4567/graphql -d '{ "query" : "mutation { mike:create(name:'Mike'){rc,id}, john:create(name:'John'){rc,id} }" }'
"{\"data\":{\"mike\":{\"rc\":\"OK\",\"id\":1},\"john\":{\"rc\":\"OK\",\"id\":2}}}"
```