Rails で GraphQL を使ってみる
概要
Ruby on Rails での GraphQL の導入方法、簡単な CRUD 操作、そもそも GraphQL とは?について書きました。RSpec まで書いてるので、これから GraphQL を使う人向けです。
※自分自身、初めて GraphQL に触れるので間違った解釈などあるかもしれません。そのときはご指摘いただけると嬉しいです。
環境
ruby 3.1.2
rails 7.0.8
graphiql-rails 1.10.1
graphql 2.3.16
準備
必要な gem をインストール
gem 'graphql'
必要なファイルを作成
rails generate graphql:install
作成と編集されたファイルは以下。
> rails generate graphql:install
[dotenv] Loaded .env
create app/graphql/types
create app/graphql/types/.keep
create app/graphql/myapp_schema.rb
create app/graphql/types/base_object.rb
create app/graphql/types/base_argument.rb
create app/graphql/types/base_field.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_scalar.rb
create app/graphql/types/base_union.rb
create app/graphql/resolvers/base_resolver.rb
create app/graphql/types/query_type.rb
add_root_type query
create app/graphql/mutations
create app/graphql/mutations/.keep
create app/graphql/mutations/base_mutation.rb
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
create app/graphql/types/node_type.rb
insert app/graphql/types/query_type.rb
create app/graphql/types/base_connection.rb
create app/graphql/types/base_edge.rb
insert app/graphql/types/base_object.rb
insert app/graphql/types/base_object.rb
insert app/graphql/types/base_union.rb
insert app/graphql/types/base_union.rb
insert app/graphql/types/base_interface.rb
insert app/graphql/types/base_interface.rb
insert app/graphql/myapp_schema.rb
insert config/application.rb
Query を実装
GraphQL の世界では、データの操作のことをオペレーション(Operation)と呼びます。その中でも、データの取得系の事を Query と呼びます。
一般的な「クエリ」は「問い合わせ」という意味なので、「Query」 と「クエリ」は分けて考えたほうが良いです。つまり、「Query の問い合わせ内容」のことを「Query のクエリ」と表現する事もあります。
クエリの型を作成
先ほど作成されてた app/graphql/types/query_type.rb
に、この Query の型を定義していきます。
型というのは、「こんなリクエストをされたらこんな値を返しますよ」という決まり事のようなものです。今回は「記事一覧取得」と「記事詳細取得」の型を定義していきます。
module Types
class QueryType < Types::BaseObject
# 記事一覧の Query
field :articles, resolver: Resolvers::Articles::GetArticles , null: false
# 記事詳細取得の Query
field :article, resolver: Resolvers::Articles::GetArticle, null: true
end
end
field メソッド
GraphQL のフィールドを定義するために使用されます。このフィールドは、クライアントがデータをリクエストする際に指定するものです。
resolver オプション
クエリの実際の処理を Resolvers::Articles::GetArticles
というリゾルバに委譲しています。リゾルバは、クエリを解決するためのロジックです。
null オプション
false
のため、このフィールドは null
を返さない(必ず何かしらの結果を返す)ことを表しています。
個人的には REST のときのエンドポイントに近いなという印象でした。
リゾルバの実装
リゾルバは Query を解決するロジックを書くところ。
Resolvers::BaseResolver
クラスを継承することにより、type
, argument
,resolve
といったメソッドを使用できます。
# 記事一覧取得のリゾルバ
# app/graphql/resolvers/articles/get_articles.rb
module Resolvers
module Articles
class GetArticles < Resolvers::BaseResolver
# 戻り値
type [Types::ArticleType], null: false
def resolve
Article.all
end
end
end
end
# 記事詳細取得のリゾルバ
# app/graphql/resolvers/articles/get_article.rb
module Resolvers
module Articles
class GetArticle < Resolvers::BaseResolver
# 戻り値
type Types::ArticleType, null: false
# 引数
argument :id, ID, required: true
def resolve(id:)
Article.find(id)
end
end
end
end
type メソッド
リゾルバで返す値の型を指定している。今回の Types::ArticleType
は以下のように別途定義しています。このように定義しておくことで、クライアント側から使用したいプロパティのみを指定して取得することができます。
# app/graphql/types/article_type.rb
module Types
class ArticleType < Types::BaseObject
# Article モデルのプロパティとその型を指定していく。
field :id, ID, null: false
field :title, String, null: false
field :body, String, null: false
field :is_published, Boolean, null: false
field :custom_title, String, null: false
# カスタムした値を定義することも可能。
def custom_title
"#{object.id}_#{object.title}"
end
end
end
記事一覧取得リゾルバで、
type [Types::ArticleType], null: false
となっていますが、これは Types::ArticleType
を要素とするリスト型(配列)であるという意味です。また、null: false
となっているので、レスポンスのこのフィールドは null
にはならない事を示しています。GraphQL のスキーマ定義言語(SDL)で書くと、[Article!]!
となります。
ちなみにリストの要素の null
を許可する場合は [Article]!
となり、Rails では以下のように書きます。
type [Types::ArticleType, null: true], null: false
argument メソッド
その名の通り引数とその型を定義しています。これを定義しておくことでクライアントから不正な値が渡されたときのチェックもしてくれるので便利です。
Mutation を実装
前述の Query に対し、GraphQL の世界ではデータの変更(作成、更新、削除)のことを Mutation と呼びます。Mutation でも「クエリの型を定義する」といった表現をするのでややこしいです。
クエリの型を定義
次は app/graphql/types/mutation_type.rb
にミューテーションの型を定義していきます。
Query のときと同じく、Types::BaseObject
を継承しているので、Rails 的には Query と Mutation でクエリの定義は別ファイルにして欲しいようです。
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
# 記事作成の Mutation
field :create_article, Types::ArticleType, mutation: Mutations::Articles::CreateArticle, null: false
# 記事更新の Mutation
field :update_article, Types::ArticleType, mutation: Mutations::Articles::UpdateArticle, null: false
# 記事削除の Mutation
field :destroy_article, Types::ArticleType, mutation: Mutations::Articles::DestroyArticle, null: false
end
end
Query のときと違うのは、resolver
オプションではなく、mutation
オプションで「リゾルバ」を指定していることです。mutation
オプションを指定すると、Mutations::BaseMutation
の機能を有している前提で処理されます。
データの変更をする場合はバリデーションなどといった、データの取得時にはしない複雑な処理が発生します。graphql-rails
ではそのような処理を分けるためにあえて、この2つのオプションでリゾルバを指定できるようになっていると考えられます。
GraphQL の世界では Query だろうと Mutation だろうとクエリを解決するロジックのことをどちらも「リゾルバ」と表現しているので、ここでも「リゾルバ」として進めます。
リゾルバの実装
データの取得時とルールは変わりません。
# 記事作成のリゾルバ
# app/graphql/mutations/articles/create_article.rb
module Mutations
module Articles
class CreateArticle < Mutations::BaseMutation
# 戻り値の定義
type Types::ArticleType
# 引数の定義
argument :title, String, required: true
argument :body, String, required: true
argument :is_published, Boolean, required: true
def resolve(title:, body:, is_published:)
Article.create!(title: title, body: body, is_published: is_published)
end
end
end
end
# 記事更新のリゾルバ
# app/graphql/mutations/articles/update_article.rb
module Mutations
module Articles
class UpdateArticle < Mutations::BaseMutation
# 戻り値の定義
type Types::ArticleType
# 引数の定義
argument :id, ID, required: true
argument :title, String, required: true
argument :body, String, required: true
argument :is_published, Boolean, required: true
def resolve(id:, title:, body:, is_published:)
article = Article.find(id)
article.update!(title: title, body: body, is_published: is_published)
article
end
end
end
end
# 記事削除のリゾルバ
# app/graphql/mutations/articles/destroy_article.rb
module Mutations
module Articles
class DestroyArticle < Mutations::BaseMutation
# 戻り値の定義
type Types::ArticleType
# 引数の定義
argument :id, ID, required: true
def resolve(id:)
article = Article.find(id)
article.destroy
article
end
end
end
end
RSpec
RSpec に関しては一般的な記述と変わりません。クエリの部分を直接指定して書いています。
今回は代表して記事詳細取得と記事作成のみ記載します。
# 記事詳細取得の RSpec
# spec/requests/graphql/articles/article_spec.rb
require 'rails_helper'
RSpec.describe 'Resolvers::Articles::GetArticle', type: :request do
subject(:exec_get_article) { post '/graphql', params: params }
let(:params) { { query: query } }
let(:query) do
<<-GRAPHQL
query {#{' '}
article(id: "#{article.id}"){
id
title
body
isPublished
}
}
GRAPHQL
end
let!(:article) { create(:article) }
context '正常系' do
it 'レスポンスが正しいこと' do
exec_get_article
json = JSON.parse(response.body)
target_data = json['data']['article']
expect(target_data['id']).to eq(article.id.to_s)
expect(target_data['title']).to eq(article.title)
expect(target_data['body']).to eq(article.body)
expect(target_data['isPublished']).to eq(article.is_published)
end
end
end
# 記事作成の RSpec
# spec/requests/graphql/articles/create_article_spec.rb
require 'rails_helper'
RSpec.describe 'Mutations::Articles::CreateArticle', type: :request do
subject(:exec_create_article) { post '/graphql', params: params }
let(:params) { { query: query } }
let(:query) do
<<-GRAPHQL
mutation {#{' '}
createArticle(input: { title: "#{title}", body: "#{body}", isPublished: #{is_published}}){
id
title
body
isPublished
}
}
GRAPHQL
end
let(:title) { 'title' }
let(:body) { 'body' }
let(:is_published) { false }
context '正常系' do
it 'レスポンスが正しいこと' do
exec_create_article
article = Article.order(:created_at).last
json = JSON.parse(response.body)
target_data = json['data']['createArticle']
expect(target_data['id']).to eq(article.id.to_s)
expect(target_data['title']).to eq(title)
expect(target_data['body']).to eq(body)
expect(target_data['isPublished']).to eq(is_published)
end
it '記事が登録されること' do
expect { exec_create_article }.to change(Article, :count).by(1)
end
end
end
テスト用のクライアントツールで動作確認する
graphiql
というブラウザで利用できる IDE を使っていきます。rails では graphiql-rails
という gem が提供されているので、そちらを使っていきます。
Gemfile
に以下を追加して bundle install
gem 'graphiql-rails'
API モードだったので表示用に sprockets
を追加し、bundle install
gem 'sprockets-rails'
公式ドキュメントどおりに以下の設定を追加。
# config/application.rb
require "sprockets/railtie"
# app/assets/config/manifest.js
//= link graphiql/rails/application.css
//= link graphiql/rails/application.js
Session, Cookie の設定を有効にする必要があるので以下を追加。
# config/application.rb
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.api_only = false
routes.rb
に以下を追加
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
/graphiql
にアクセス。
以下のような画面が表示され、作成したスキーマの動作確認をすることができます。
まとめと感想
今回は Ruby on Rails での CURD 操作を実装してみました。
REST に比べると、フロントエンド側は使用したいフィールドのみを取得できるので、オーバーフェッチング、アンダーフェッチングにならず、API の呼び出し回数の削減に繋がりますね。
バックエンド側はクライアント側に特化した API を作らなくても良くなりますね。よく画面ごとに必要な API を作ることが多いですが、メンテナンス性も悪く、開発を続けると一貫性が無くなってきます。
まだ細かい部分の理解はできてないので、引き続きキャッチアップしていきます。
Discussion