はじめに
Ruby on RailsでGraphQLに入門してみたのでその備忘録です。
環境は以下の通りです。
- ruby: 3.0.2
- rails: 6.1.4
- graphql-ruby: 1.12
- graphiql-rails: 1.8.0
GraphQLとは
GraphQLはAPI用のクエリ言語で、REST APIと違って以下のようなメリットがあります。
- 1つのエンドポイントのみで呼び出せる
- 欲しい情報のみを指定でき、余分なものを取得しない
セットアップ
こちらを参考にRailsにGraphql、GraphiQLを導入していきます。
GraphiQLを導入することで、develop環境においてGUI上でGraphqlのqueryやmutaitonの設定の確認や実際に実行してテストといったことができるようになります。
phpMyAdminのGraphql版みたいなイメージです。
gem 'graphql'
group :development do
gem 'graphiql-rails'
end
bundle install
rails g graphql:install
ルーティングにGraphiQLの設定を行います。
これでdevelop環境でのみhttp://localhost:3000/graphiqlのようなURLでGraphiQLにアクセスできるようになります。
Rails.application.routes.draw do
post "/graphql", to: "graphql#execute"
# 以下を追加
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end
end
データの取得(Query)
データを取得するにはTypeの定義とQueryの定義をする必要があります。
Typeの定義
Typeはレスポンスで返すデータの構造を定義する為のものです。
Type定義用のファイルを生成する前にrailsのモデルを作成し、マイグレーションを実行しておくと、そのスキーマを元にTypeの定義を自動生成してくれます。
rails g model User
# migrationの設定
rails db:migrate
rails g graphql:object User
ファイルが作成され、例えば以下のようなコードが生成されます。
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :email, String, null: false
field :encrypted_password, String, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
但し、Typeはレスポンスで返すデータ構造の定義なので、DBのテーブル構造と一致している必要はなく、自由にfieldを追加・削除することができます。
パスワードのようなAPIに載せない方が良い情報はfieldを削除しておいた方が良さそうだと思います。
Queryの定義
実際にどうやって情報を取得するかという部分をQueryで定義します。
Queryの定義はapp/graphql/types/query_type.rb
に書いていきます。
例えば全てのユーザーを取得するQueryや指定したIDのユーザーを取得するQueryは以下のように書けます。
module Types
class QueryType < Types::BaseObject
field :users, [UserType], null: false
def users
User.all
end
field :user, UserType, null: false do
argument :id, ID, required: true
end
def user(id:)
User.find(id)
end
end
end
GraphiQLでデータ取得してみる
GraphiQLのページにアクセスし、左の入力欄に以下のように入力して再生ボタンを押してQueryを選択すると右の欄に結果が表示されます。
定義の時はフィールド名をsnake_caseで記述していましたが、APIで呼び出す際はcamelCaseで記述します。
query users {
users {
id
email
createdAt
}
}
query user1 {
user(id: 1) {
id
email
}
}
query user($id: ID!) {
user(id: $id) {
id
email
}
}
{
"id": "1"
}
一番下のQueryはパラメータを分けて書く書き方です。
型名(ID!
)の後ろの!
は必須パラメータであることを表しています。
(query定義でargument :id, ID, required: true
としている為)
また、Queryや後述するMutation、Typeの定義は右上のDocsから見ることができます。
データの作成・編集・削除(Mutation)
データの作成・編集・削除など副作用のあるような処理はMutationで定義します。
mutationの定義はapp/graphql/types/mutation_type.rb
に記述します。
ただ、コマンドで mutationを生成するとmutation_type.rb
にはコードが自動で追記されるので、実際に編集するのは個々のmutaion定義ファイルになります。
rails g graphql:mutation CreateUser
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser # 自動で追記される
# ~
end
end
mutationを以下のように定義していきます。
fieldでレスポンス、argumentで引数、 resolveで処理を定義します。
module Mutations
class CreateUser < BaseMutation
field :user, Types::UserType, null: false
argument :email, String, required: true
argument :password, String, required: true
argument :password_confirmation, String, required: true
def resolve(**args)
user = User.create!(args)
{ user: user }
end
end
end
GrapiQLからデータ作成してみる
mutationは以下のように呼び出します。
データ取得時と違い、mutation
から始まり、createUser()
の中がcreateUser(email: ~)
ではなくcreateUser(input:{email: ~})
という構造になっています。
mutation createUser($email: String!, $password: String!, $passwordConfirmation: String!) {
createUser(
input: {email: $email, password: $password, passwordConfirmation: $passwordConfirmation}
) {
user {
id
email
}
}
}
{
"email": "test2@example.com",
"password": "abc123",
"passwordConfirmation": "abc123"
}
編集・削除
編集・削除もデータ作成時と同様の方法でMutationを作成すればOKです。
その他
アソシエーション
例えばUserが複数のTaskのデータを持っていて、Userの情報とUserが保有するTaskを同時に取得したい場合の方法です。
Taskのmodelを作成し、migrationの実行・Typeの作成、モデルのアソシエーションの設定(Userモデル・Taskモデル)を済ませておきます。
あとはUserのTypeの定義にtasksフィールドを追加するだけです。
module Types
class UserType < Types::BaseObject
# ~
field :tasks, [TaskType], null: false
end
end
これで関連モデルの情報も一気に取れるようになります。
query user1 {
user(id: 1) {
id
email
tasks {
id
}
}
}
関連モデルの個数fieldの追加、N+1問題について
Userが保有するTaskの個数を取得したいといった場合の方法です。
方法1: Typeに取得方法含めて定義する方法
object.~
でモデルインスタンスへの処理を書けます。
module Types
class UserType < Types::BaseObject
# ~
field :tasks_count, Int, null: false
def tasks_count
object.tasks.size
end
end
end
N+1問題を回避する為に、Queryの定義の箇所でincludes(:tasks)
しておきます。
includes
していてもType定義でobject.tasks.count
にするとN+1問題が発生します。
module Types
class QueryType < Types::BaseObject
# ~
field :users, [UserType], null: false
def users
User.includes(:tasks)
end
# ~
end
end
方法2: Queryの定義内で取得する方法
module Types
class UserType < Types::BaseObject
# ~
field :tasks_count, Int, null: false
end
end
module Types
class QueryType < Types::BaseObject
# ~
field :users, [UserType], null: false
def users
User.select(%|
*,
(SELECT COUNT (*) FROM tasks t
WHERE t.user_id = users.id
) AS tasks_count
|)
end
# ~
end
end
発行されるSQL文はこちらの方が好きだし、パフォーマンスもおそらくこちらの方が良いが、正直この方法は可読性やQuery・Mutation毎に書かないといけないといったことを考えると微妙かなと感じました。
その他の方法
私はまだ試せていませんが、graphql-batchというgemを使用すればN+1問題が起こさずにより柔軟にデータ取得できるようになる?みたいです。
session、current_user
QueryやMutationの定義内で直接sessionやdeviseのcurrent_userのようなヘルパーメソッドを呼び出すことはできません。
これらを呼び出せるようにするにはGraphqlControllerで設定する必要があります。
class GraphqlController < ApplicationController
# ~
def execute
# ~
context = {
session: session,
current_user: current_user,
}
# ~
end
# ~
end
このように設定するとqueryやmutation内でcontext[:session]
やcontext[:current_user]
のようにして呼び出せるようになります。
さいごに
今回はじめてGraphQLを使ってみて、基本的なデータの取得・作成・編集・削除等は簡単に実装できたのですが、N+1問題の解消時や今回記事には書けていませんが認証や認可もやってみて詰まる部分が多くありました。今もまだまだ理解できていない部分が多くあるので、今後もGraphQLの勉強を続けていきたいと思います。
最後までお読み頂きありがとうございました。