注意
現在の graphql-ruby (ver: 1.8.x) は Ruby のクラスベースによる定義がメインで、この記事に書いてある DSL を使った定義は 非推奨 です。
ロードマップ には DSL を使った .define-style
は、graphql-ruby 2.0 で廃止するとあります。
新しいクラスベースでの書き方は 公式ドキュメント を御覧ください。
日本語の記事だと @gfx さんの 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ が非常によくまとまってて分かりやすいです。
はじめに
私が携わっているプロダクトでは、フロントエンド用のAPIとしてGraphQLを採用しました。
実際に使ってみてかなり良い感じなのですが、最初はいざ実装しようにもよく分からずに苦労しました。
そこで、GraphQL の実装方法についてサーバーサイドに焦点を当てて書いていきたいと思います。
ちなみに基礎編とありますが、基礎的な内容なので基礎編です。応用編があるかは分かりません。(もしかしたら書くかも)
GraphQLはいいぞ。
GraphQLとは何か
公式の定義によると「自分のAPIに対して使えるクエリ言語の一種」です。
他にも
- https://qiita.com/icoxfog417/items/92214aed64f47dfeade5
- http://blog.bitjourney.com/entry/2017/08/19/161308
などを読むと、大体どんなものか掴めると思います。
個人的に実装していて思うのは、GraphQLはRESTの制約をガチガチに固めた上でAPIとエンドポイントとの固い結合を解いたものだな〜ということです。
GraphQLではデータの取得や更新を基本的にPOSTで行います (仕様ではGETも定められています)。
このPOSTのリクエストパラメーターにクエリを生やすことで、APIに対して行いたい処理を指定します。
この時クライアント側は、クエリを好きな組み合わせで、欲しいデータや更新したいデータのみを指定して、APIを叩くことができます。そのため、たとえフロント側の構成やページ遷移が決まっていなくても、バックエンドはAPIの開発をし続けることができます。
このように、クライアントからは扱いたいデータを自由に選択でき、バックエンドは実装の負荷を下げられるのがGraphQLの良いところです。
GraphQLの実装
GraphQL自体はあくまで仕様で、実装は様々なものがあります。以下に一例を挙げます。
(awesome-graphqlにGraphQLの実装の一覧が載っています。)
これらは大まかに2種類に分けることができます。Graphql RubyやGraphQL.jsは既存のサーバーサイドフレームワークと組み合わせて使うのですが、GraphcoolやAppSyncはそれそのもので完結していて比較的簡単にAPIサーバーを実装することができます。Graphcoolは最近オープンソースになったのでイケイケです。
今回はRailsと組み合わせて使うことができるGraphQL Rubyで話を進めていきます。
GraphQLを書いてみる
GraphQLがどういったものか、というのは実際に書いてみるのが一番理解しやすい(説明しやすい)ので、ここからはサンプルアプリを作りながら解説していきます。
サンプルアプリはこちらです -> https://github.com/sukechannnn/graphql-ruby-demo/
plain-rails
ブランチにGraphQLを実装していない状態を残しておきました。これを使って一緒にGraphQL APIを実装してみましょう!(事前にbin/setup
を実行しておいてください。)
Install
まずはgraphql
gemをインストールします。
gem 'graphql', '1.7.7'
$ bundle install
Hello World!
Generatorがあるので、それを使ってgraphql-rubyの雛形を作りましょう。
$ rails g graphql:install
すると、このようなディレクトリツリーが出力されます。中身は後で説明します。
app/graphql
├── graphql_ruby_demo_schema.rb
├── mutations
└── types
├── mutation_type.rb
└── query_type.rb
また、graphiql
というブラウザ上でGraphQL APIを動かして動作を確認できるツールがあります。それのRails版をdevelopment groupにインストールしましょう。
gem 'graphiql-rails'
$ bundle install
ルーティングにgraphiqlのパスを追加します。
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end
はい、ここまでで一度rails s
して http://localhost:3000/graphiql にアクセスしてみてください。
graphiqlの画面が表示されていれば設定は完了です。試しにクエリを投げてみましょう。
{
testField
}
Hello World!
が返ってくれば設定は完了です!
QueryとMutationとSubscription
GraphQLで問い合わせをするための最も基本的な概念としてQueryとMutationとSubscriptionがあります。
Query
データを取得するための仕組みです。
Mutation
データを更新するための仕組みです。
Subscription
Websocketを扱うための仕組みです。ここでは触れません。
Query
ここからは実際にQueryを定義してデータを取得できるようにしていきます。まずはseed_fuで入れたデータ(db/fixtures/sample_data.rb
)を取得できるようにしましょう。
UserType
まずはUserモデルからです。graphql-rubyにはgeneratorがあるのでそれを使います。
$ rails g graphql:object User id:ID! name:String! email:String!
すると、以下のようなファイルがapp/graphql/types/user_type.rb
に生成されます。(見やすさのために少し整形しています)
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID
field :name, !types.String
field :email, !types.String
end
簡単に説明します。ここではTypes::UserType
をGraphQL::ObjectType
で定義しています。ObjectType
は基本となるオブジェクト型で、これからもよく出てきます。
name
はschemaの定義に用います。詳しくは後で説明します。
field
はどのデータにアクセスするかを定義しています。graphql-rubyでは対応するモデルのattributeと同じ名前でfieldを定義すると、それだけでデータを取得することができます。
これでUserType
を定義することはできたのですが、データを取得するためにはこれをQueryと紐付けないといけません。(逆に言うと定義した型は再利用が可能です。)
Queryと紐付けるためには最初にgenerateしたTypes::QueryType
内にfieldを書いていきます。
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
field :user, !Types::UserType do
resolve ->(_obj, _args, ctx) {
ctx[:current_user]
}
end
end
ここで、resolve
というものが出てきました。これはGraphQL内でロジックを書くための仕組みです。graphql-rubyでは単純にfieldのみを書くと、そのattribute(またはメソッド)が生えていない場合にエラーで落ちます。また、attributeが生えている場合でもデータを加工して返したい場合もあると思います。そのような時にこのresolveを使うことで解決することができます。
resolveに渡すlambdaには obj
, args
, ctx
という3つの引数があります。
-
obj
は自身のオブジェクトで、例えばUserTypeであればuserの情報が入っています- QueryTypeでは使いません
-
args
はfieldに渡す引数です- ここでは触れません、詳しくはこちら
-
ctx
はログインユーザーの情報など重要な情報を渡すために使います
ということでctx
がとても重要です。ログイン機能が付いているアプリではcurrent_user
を渡してあげるのが常套手段です。このctx
はgraphql-rubyがgenerateしたGraphqlController
で指定します。見てみるとコメントにそれらしいことが書いてあるのが分かると思います。今回はログイン機能がないので、User.last
をcurrent_user
とすることとします。
class GraphqlController < ApplicationController
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: User.last, # ここでcurrent_userを指定する
}
result = GraphqlRubyDemoSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
end
...
はい、ここまで来ればGraphQLで値を取得することができます。graphiqlから以下のクエリを投げてみてください。seedで登録したデータが返ってくるはずです。
{
user {
id
name
email
}
}
graphql-rubyではこんな感じのDSLでAPIを定義していきます。最初は違和感があると思いますが、慣れるとフォーマットがある程度決まっているので簡単です。(最近Classで定義できるPRがマージされてバージョン1.8でリリースされる予定なので、DSLが気持ち悪いと感じた方はもう少しお待ち下さい。)
AddressType
次に、Userと1対1で紐付いているAddressにアクセスできるようにします。まずはAddressType
を作りましょう。
$ rails g graphql:object Address id:ID! postal_code:Int! address:String!
もしActiveRecordを使ってAddressを取得する場合、直接Addressを取りに行くのではなくUserから辿ると思います。それはGraphQLでも同じです。なので、Userにfieldを生やしてあげます。
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID
field :name, !types.String
field :email, !types.String
field :address, !Types::AddressType # 追加する
end
これでAddressを取得できるようになりました。graphiqlから以下のクエリを投げてみてください。
{
user {
id
name
email
address {
postal_code
address
}
}
}
住所と郵便番号が返ってきたでしょうか。これでQueryの基本的な部分の説明はだいたい終わりました。最後にConnectionについて軽く触れておきます。
Connection
モデルにはUserがつぶやきを投稿できるような場合を想定して、PostがUserに対して1対多で生えています。これを取得できるようにしたいです。なにはともあれPostType
をgenerateしましょう。
$ rails g graphql:object Post id:ID! subject:String!
UserTypeに対してPostTypeが複数あるような場合、fieldではなくconnection
を使って関連を表します。
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID
field :name, !types.String
field :email, !types.String
field :address, !Types::AddressType
connection :posts, !Types::PostType.connection_type # 追加する
end
これでPostsをまとめて取れるようになりました。graphiqlから以下のクエリを投げてみてください。
{
user {
id
name
email
posts {
edges {
node {
id
subject
}
}
}
}
}
Postの一覧が取得できたと思います。並び順を変えたい場合は、Resolverを使ってorder_by
すれば良い感じになります。詳しくはこちらをご覧ください。
Connectionを使うとページネーションの仕組みも一緒に付いてくるので非常に便利です。Connectionのクライアント側からの詳しい使い方については下記を参照してください。
https://facebook.github.io/relay/graphql/connections.htm
いくつか補足
ここまで駆け足で説明してきたので、いくつか説明が抜けている部分があります。それらをここで簡単に補足します。
型
GraphQLには型があります。
-
Int
: A signed 32‐bit integer -
Float
: A signed double-precision floating-point value -
String
: A UTF‐8 character sequence -
Boolean
: true or false -
ID
: ユニークな文字列で、CacheやConnectionなどRelay由来の仕組みに使われます
また、独自にScalarTypeを定義することもできます。
Relayとは
RelayはFacebookが開発しているGraphQLを使ったFluxの実装です。ReactとGraphQLの性能を最大限引き出すことができるようで、GC等のかなり強力な機能が付いています。FluxなのでReduxと組み合わせて使うことはできません。Redux&ReactとGraphQLを組み合わせたい場合はApolloを使うといいです。
Relayは独自にGraphQLを拡張しているのですが、いくつかの機能がGraphQLにも取り入れられています。特にConnectionType(ページネーションの仕組み)はGraphQL本体の仕様になったと言っても過言ではありません。
型に付いてる!マーク
field :name, !types.String
の!
マークはnullable falseを表します。!
マークが付いていなければ、そのfieldはnullableです。基本的にnullable falseで定義していくのが良いと思います。
Mutation
ここからはMutationを定義してデータを更新できるようにします。これから説明するのはRelay由来の方のMutationです。こちらの方がすっきり書けて見通しがよく、またコードの再利用ができるようになっています。元のMutationはこちらが理解できれば簡単に書けるので説明は割愛します。
Mutations::UpdateAddressMutation
まずはAddressを変更できるようにしてみましょう。Mutationのファイルをgenerateします。
$ rails g graphql:mutation UpdateAddressMutation
すると、コメントにinput_field
とreturn_field
とresolve
を定義するように書いてあります。これらを定義してあげればMutationが作れそうです。
input_field
はそのままで、Mutationに使うインプットです。今回は変更したい住所と郵便番号を設定します。
return_field
は返り値です。アップデートした後の各fieldの値を返すために、Queryで使うために定義したTypes::AddressType
を使います。
そして、resolve
でアップデートのロジックを書くことで実装は完了します。
Mutations::UpdateAddressMutation = GraphQL::Relay::Mutation.define do
name 'UpdateAddress'
input_field :postal_code, !types.Int
input_field :address, !types.String
return_field :address, !Types::AddressType
resolve ->(_obj, inputs, ctx) {
begin
address = ctx[:current_user].address
address.postal_code = inputs.postal_code
address.address = inputs.address
address.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ address: address }
}
end
{ address: address }
はreturn_field
で返す値で、Hashで定義します。また、エラーハンドリングで投げる例外はGraphQL::ExecutionError
を使うのが良いです。
では、実際にアップデートしてみましょう。graphiqlから以下のクエリを投げてみてください。返ってきた値が更新後のものになっていれば正常に動作しています。
mutation {
updateAddressMutation(input: {
postal_code: 1638001
address: "東京都新宿区西新宿2丁目8−1"
}) {
address {
id
postal_code
address
}
}
}
Mutations::CreatePostMutation
先程定義したQueryのPostType
を投稿できるようにしてみましょう。Mutationのファイルをgenerateします。
$ rails g graphql:mutation CreatePostMutation
先程と同じように中身を書いていきます。今度はCreateなので、Postをbuildして値を入れ、保存します。
Mutations::CreatePostMutation = GraphQL::Relay::Mutation.define do
name 'CreatePost'
input_field :subject, !types.String
return_field :post, !Types::PostType
resolve ->(_obj, inputs, ctx) {
begin
post = ctx[:current_user].posts.build
post.subject = inputs.subject
post.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ post: post }
}
end
できたらgraphiqlから投稿してみましょう!
mutation {
createPostMutation(input: {subject: "良い感じ!"}) {
post {
subject
}
}
}
current_userのpostsのqueryを投げてみて、postが追加されていることを確認してみてください。
{
user {
id
name
email
posts {
edges {
node {
id
subject
}
}
}
}
}
Mutations::UpdatePostMutation
今度は投稿したpostを更新してみましょう!UpdatePostMutation
をgenerateします。
$ rails g graphql:mutation UpdatePostMutation
今度は更新したいpostを指定する必要があるため、input_field
にid
を追加します。それ以外はやることは先程とほとんど同じです。
Mutations::UpdatePostMutation = GraphQL::Relay::Mutation.define do
name 'UpdatePost'
input_field :id, !types.ID
input_field :subject, !types.String
return_field :post, !Types::PostType
resolve ->(_obj, inputs, ctx) {
begin
post = ctx[:current_user].posts.find(inputs.id)
post.subject = inputs.subject
post.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ post: post }
}
end
あとはTypes::MutationType
にfieldを追加したら、更新してみましょう!
mutation {
updatePostMutation(input: {id: "{先程投稿したpostのid}", subject: "めっちゃ良い感じ!!!"}) {
post {
subject
}
}
}
InputType
上記のMutationはinput_fieldの項目が少なかったので良かったのですが、それが多くなってくると全部をMutationに書いていくのはしんどくなってきます。後ほど説明するdescription
を追加していくとコードの量が一気に増えます。また、同じモデルに対するCreateやUpdateではinput_fieldが同じになることも多く、使いまわせたら便利です。それをするための仕組みがInputType
です。試しにUpdateAddressMutation
のInputTypeを定義してみましょう。新規ファイルapp/graphql/types/address_input_type.rb
に以下のように実装します。
Types::AddressInputType = GraphQL::InputObjectType.define do
name 'AddressInput'
argument :postal_code, !types.Int
argument :address, !types.String
end
そうしたら、UpdateAddressMutation
を以下のように変更します。
Mutations::UpdateAddressMutation = GraphQL::Relay::Mutation.define do
name 'UpdateAddress'
input_field :addressInput, !Types::AddressInputType # input_fieldにTypes::AddressInputTypeを指定する
return_field :address, !Types::AddressType
resolve ->(_obj, inputs, ctx) {
address_input = inputs.addressInput # input_fieldのネストが一段深くなるので、addressInputを取り出す
begin
address = ctx[:current_user].address
address.postal_code = address_input.postal_code
address.address = address_input.address
address.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ address: address }
}
end
これで、下記のようにUpdateAddressMutationのクエリを投げることができます。(以前とちょっと違う)
mutation {
updateAddressMutation(input: {addressInput: {postal_code: 1638001, address: "東京都新宿区西新宿2丁目8−1"}}) {
address {
id
postal_code
address
}
}
}
GraphQLのテスト
Request spec
GraphQLの挙動を確認するテストはrequest specを書いていくのが良さそうです。graphiqlから投げているqueryをテスト内で同じように使うことで、実際の挙動をテストすることができます。
説明はともかく実装を見てみましょう。spec/requests/graphql/query/user_spec.rb
に以下のようなテストを書きます。
require 'rails_helper'
RSpec.describe 'user query', type: :request do
subject { post graphql_path, params: { query: query } }
let!(:user) { FactoryBot.create(:user) }
let(:query) do
<<~QUERY
{
user {
id
email
name
}
}
QUERY
end
it 'response body is User data' do
subject
json = JSON.parse(response.body, symbolize_names: true)
expect(json[:data][:user][:id]).to eq user.id
expect(json[:data][:user][:email]).to eq user.email
expect(json[:data][:user][:name]).to eq user.name
end
end
これで意図したとおりのQueryが投げられているかを確認できます。ここまで必要ないという意見もあると思いますが、個人的には必要なテストだと思いますし、これが書いてあると後から見た時に挙動が一発で分かるので便利です。Mutationのテストも同じように書くことができます。試してみてください。
graphql-rubyのschemaが正しいか確認するテスト
graphql-rubyでは、実装からschema.graphqlを機械的に出力することができます。このschema.graphqlはそのままドキュメントになるので、きちんと追従していれば実装とドキュメントがかけ離れるということがありません。追従するに当たってはCIを使って自動更新することも出来ると思うのですが、ここでは簡単に対処するために最新のschemaになっていなかったらテストで落ちるようにするやり方を説明します。
schema.graphql
その前にschema.graphqlです。先程も言ったとおりこれはドキュメントになるのですが、そのためにはdescriptionを書く必要があります。今までの説明ではすっ飛ばしてきましたが、QueryやMutationを定義する時にdescriptionを追加します。試しにQueryType
とUserType
に書いてみましょう。
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
field :user, !Types::UserType do
description 'You can access current_user' # description を追加
resolve ->(_obj, _args, ctx) {
ctx[:current_user]
}
end
end
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID, 'ユニークな ID'
field :name, !types.String, '名前'
field :email, !types.String, 'e-mail アドレス'
field :address, !Types::AddressType, '住所'
connection :posts, !Types::PostType.connection_type, '投稿一覧'
end
descriptionの書き方はブロックを使うかどうかで変わりますが、やりたいことは同じです。この状態でschema.graphqlをgenerateしてみましょう。まずはgraphql-rubyのrake taskを呼べるようにRakefileに追記します。
require_relative 'config/application'
require 'graphql/rake_task' # 追記
Rails.application.load_tasks
GraphQL::RakeTask.new(schema_name: 'GraphqlRubyDemoSchema') # 追記
...
これでschema.graphqlをdumpできるようになったので、rake taskを実行しましょう。
$ rake graphql:schema:dump
schema.graphql
とschema.json
がdumpされたと思います。schema.json
の方はschemaの全ての情報がjsonで網羅されています(graphiqlで使われてるんだっけな...よくわかってません)。
schema.graphqlのtype Query
とtype User
を見てみると、先程書いたdescriptionが出力されていることがわかります。この型の定義と説明が合わさったschemaを実装から機械的に生成できるのは、json schemaやswaggerに対して便利な点だと感じています。
schema.graphqlのテスト
機械的に出力されたschemaも最新でなければ意味がありません。そこで、実装から出力される最新のschemaと既に出力済みのschemaで差分がないかどうかをチェックするテストを書き、最新でなければエラーになるようにします。エラーで落ちたら改めてschemaをdumpすればOKです。以下のようなテストを追加します(私はずぼらなのでspec/requests/graphql/graphql_ruby_demo_schema_spec.rb
に追加しました)。
require 'rails_helper'
RSpec.describe 'GraphqlRubyDemoSchema' do
let(:current_definition) { GraphqlRubyDemoSchema.to_definition }
let(:printout_definition) { File.read(Rails.root.join('schema.graphql')) }
it 'equals dumped schema, `rake graphql:schema:dump` please!' do
expect(current_definition).to eq(printout_definition)
end
end
試しにどこかのタイプを変更してテストを実行してみてください。エラーで落ちたらdumpして再度テストを実行してみてください。テストが通ると思います。
これにより、schemaが古い状態であることに必ず気づくことができ、ドキュメントを常に実装と一致させることができます。また、意図した変更であるかどうかも同時に確認することができます。これ実際に使っていて非常に便利なので、是非使ってみてください!
最後に
GraphQLは公式のドキュメントがかなりしっかりしているので、それを読めば必要なことは全部書いてあります。書いてあるのですが割と量が多いので、最初は概要を掴むのに苦労しました。
なので、ざっくり全体像が分かるような記事があると良いなーと思い、これを書きました。最初の入り口として参考になれば幸いです!
graphql-rubyの機能は他にもたくさんありますし、書き方も様々です。まだまだベストプラクティスが定まっていない新しい技術なので、みんなで触ってより良くしていきましょう!