3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

株式会社mofmofAdvent Calendar 2021

Day 13

RailsでGraphQL(graphql-ruby)入門備忘録

Last updated at Posted at 2021-12-12

はじめに

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版みたいなイメージです。

Gemfile
gem 'graphql'
group :development do
  gem 'graphiql-rails'
end
terminal
bundle install
rails g graphql:install

ルーティングにGraphiQLの設定を行います。
これでdevelop環境でのみhttp://localhost:3000/graphiqlのようなURLでGraphiQLにアクセスできるようになります。

config/routes.rb
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の定義を自動生成してくれます。

terminal
rails g model User
# migrationの設定
rails db:migrate
rails g graphql:object User

ファイルが作成され、例えば以下のようなコードが生成されます。

graphql/types/user_type.rb
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は以下のように書けます。

app/graphql/types/query_type.rb
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で記述します。

graphql
query users {
  users {
    id
    email
    createdAt
  }
}

query user1 {
  user(id: 1) {
    id
    email
  }
}

query user($id: ID!) {
  user(id: $id) {
    id
    email
  }
}

QUERY_VARIABLES
{
  "id": "1"
}

一番下のQueryはパラメータを分けて書く書き方です。
 型名(ID!)の後ろの!は必須パラメータであることを表しています。
(query定義でargument :id, ID, required: trueとしている為)
また、Queryや後述するMutation、Typeの定義は右上のDocsから見ることができます。

41c85dc040331a98c0eaf281be2a9a48.gif

データの作成・編集・削除(Mutation)

データの作成・編集・削除など副作用のあるような処理はMutationで定義します。
mutationの定義はapp/graphql/types/mutation_type.rbに記述します。
ただ、コマンドで mutationを生成するとmutation_type.rbにはコードが自動で追記されるので、実際に編集するのは個々のmutaion定義ファイルになります。

terminal
rails g graphql:mutation CreateUser
app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :create_user, mutation: Mutations::CreateUser # 自動で追記される
    # ~
  end
end

mutationを以下のように定義していきます。
fieldでレスポンス、argumentで引数、 resolveで処理を定義します。

app/graphql/mutations/create_user.rb
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: ~})という構造になっています。

graphql
mutation createUser($email: String!, $password: String!, $passwordConfirmation: String!) {
  createUser(
    input: {email: $email, password: $password, passwordConfirmation: $passwordConfirmation}
  ) {
    user {
      id
      email
    }
  }
}

QUERY_VARIABLES
{
  "email": "test2@example.com",
  "password": "abc123",
  "passwordConfirmation": "abc123"
}

編集・削除

編集・削除もデータ作成時と同様の方法でMutationを作成すればOKです。

その他

アソシエーション

例えばUserが複数のTaskのデータを持っていて、Userの情報とUserが保有するTaskを同時に取得したい場合の方法です。

Taskのmodelを作成し、migrationの実行・Typeの作成、モデルのアソシエーションの設定(Userモデル・Taskモデル)を済ませておきます。
あとはUserのTypeの定義にtasksフィールドを追加するだけです。

graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    # ~
    field :tasks, [TaskType], null: false
  end
end

これで関連モデルの情報も一気に取れるようになります。

graphql
query user1 {
  user(id: 1) {
    id
    email
    tasks {
      id
    }
  }
}

関連モデルの個数fieldの追加、N+1問題について

Userが保有するTaskの個数を取得したいといった場合の方法です。

方法1: Typeに取得方法含めて定義する方法

object.~でモデルインスタンスへの処理を書けます。

graphql/types/user_type.rb
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問題が発生します。

app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    # ~
    field :users, [UserType], null: false
    def users
      User.includes(:tasks)
    end
    # ~
  end
end

方法2: Queryの定義内で取得する方法

graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    # ~
    field :tasks_count, Int, null: false
  end
end

app/graphql/types/query_type.rb
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で設定する必要があります。

app/controllers/graphql_controller.rb
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の勉強を続けていきたいと思います。
最後までお読み頂きありがとうございました。

参考にさせていただいた記事

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?