🦓

[Rails]GraphQLを導入する

2024/02/15に公開

はじめに

RailsアプリにGragphQLを導入する時のセットアップについてまとめてみました。

GraphQL

GraphQLは、クライアントとサーバー間のデータクエリと操作のためのクエリ言語です。
GraphQLの基本的なコンセプトは以下になります。

  1. スキーマ定義:

GraphQLの最初のステップは、データ構造を定義するスキーマを作成することです。これは、GraphQLサーバーがクエリを理解し、クライアントに返すデータの形式を定義します。

type Query {
  hello: String
}
  1. データ取得のクエリ:

クライアントは、GraphQLクエリを使用してデータを取得します。クエリは、サーバーに要求されたデータの形式を指定します。

query {
  hello
}
  1. サーバー側の処理:

サーバーは、クエリを受け取り、それに対応するデータを取得するためのリゾルバ関数を実行します。

class QueryType < GraphQL::Schema::Object
  field :hello, String, null: false

  def hello
    'world'
  end
end
リゾルバ

リゾルバは、GraphQLスキーマ内の特定のフィールドに対するデータの取得や操作を行うためのメソッドです。GraphQLスキーマ内で定義された各フィールドに対して1つずつ存在します。
クライアントが特定のフィールドを要求した場合、対応するレゾルバが呼び出され、データベースクエリの実行や外部APIとの通信など、必要な処理を行いそのフィールドに対するデータを返します。

  1. データベースクエリの実行: データベースから必要な情報を取得するためのクエリを実行します。これには、ActiveRecordやSQLクエリを使用する場合があります。
def resolve
  # データベースからユーザー情報を取得するクエリを実行する例
  User.find(args[:id])
end
  1. 外部APIとの通信: 外部のWebサービスやマイクロサービスからデータを取得するためのHTTPリクエストを送信します。この場合、通常はHTTPクライアントライブラリ(例: Faraday、HTTParty)を使用します。
def resolve
  # 外部APIからユーザー情報を取得するHTTPリクエストを送信する例
  response = HTTParty.get("https://api.example.com/users/#{args[:id]}")
  JSON.parse(response.body)
end
  1. ビジネスロジックの実行: データの加工や計算、認証や権限のチェックなど、ビジネスロジックを実行することもあります。
def resolve
  # ユーザーが特定の権限を持っているかどうかをチェックする例
  if context[:current_user].admin?
    # 管理者の場合はすべてのユーザーを取得する
    User.all
  else
    # 一般ユーザーの場合は自分の情報のみを返す
    context[:current_user]
  end
end
  1. レスポンス:

サーバーは、クライアントに対して要求されたデータを含むレスポンスを返します。

{
  "data": {
    "hello": "world"
  }
}

まとめると、クライアントがGraphQLクエリを送信し、サーバーがそのクエリを処理し、要求されたデータを返すプロセスが行われます。

GraphQLとREST APIの主な違い
  1. データの取得方法

    • GraphQL:クライアントが必要なデータの構造をクエリで指定できます。クライアントは1つのクエリで複数のリソースを取得し、不要なデータを取得することなく必要なデータのみを取得できます。
    • REST API:固定されたエンドポイントが特定のデータリソースを返します。これにより、必要なデータを取得するために複数のエンドポイントをリクエストする場合があります。
  2. データの取得粒度

    • GraphQL:クライアントが必要なフィールドのみを取得できるため、オーバーフェッチングやアンダーフェッチングの問題が解消されます。
    • REST API:エンドポイントごとに固定されたデータの形式を提供するため、クライアントはリソースごとに固定されたデータを取得することになります。
  3. バージョン管理

    • GraphQL:スキーマのバージョン管理を通じて、新しいフィールドやタイプを追加し、古いフィールドを廃止することができます。
    • REST API:通常、エンドポイントのバージョン管理を使用して、APIの変更を管理します。
  4. ネットワークトラフィック

    • GraphQL:1つのクエリで複数のリソースを取得できるため、ネットワークトラフィックが削減されます。
    • REST API:複数のエンドポイントをリクエストするため、ネットワークトラフィックが増加する場合があります。
  5. キャッシュ

    • GraphQL:クエリの結果をキャッシュすることが困難です。クエリは常にユニークなため、キャッシュの再利用が難しい場合があります。
    • REST API:特定のエンドポイントからのレスポンスはキャッシュされるため、再利用が容易です。

tl;dr

  1. Docker+Rails+postgresqlで環境構築
  2. graphqlをインストールする
  3. モデルを作成する
  4. typeを作成する
  5. QueryTypeを定義する
  6. 検証

Railsアプリを作成し、GraphQLを通してサーバーからユーザーを取得するまでやっていきます。

Docker+Rails+postgresqlで環境構築

Dockerfile
Dockerfile
FROM ruby:3.1.2-alpine3.15

ENV LANG="ja_JP.UTF-8"
ENV app="/app"

WORKDIR $app

RUN apk update \
 && apk upgrade \
 && apk --update add \
      g++ \
      make \
      tzdata \
      postgresql-dev \
      openssh \
      git \
      bash \
 && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
 && rm -rf /var/cache/apk/*
RUN gem update --system \
 && gem install bundler --no-document

EXPOSE 3000
docker-compose.yml
docker-compose.yml
version: '3.8'
services:
  db:
    image: postgres:15.3-alpine3.18
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - db-data:/var/lib/postgresql/data
    ports:
      - 5432:5432
    env_file:
      - .env

  web:
    build:
      context: .
    environment:
      - TZ=Asia/Tokyo
      - BUNDLE_APP_CONFIG=/app/.bundle
    env_file:
      - .env
    ports:
      - 3000:3000
    volumes:
      - .:/app
      - bundle-data:/usr/local/bundle
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b 0.0.0.0 -p 3000 -e development"
    stdin_open: true
    tty: true
    depends_on:
      - db

volumes:
  db-data:
    driver: local
  bundle-data:
    driver: local
.env
.env
# DB
POSTGRES_USER=user
POSTGRES_PASSWORD=password

docker-compose buildを実行します。

docker-compose build
ターミナル
➜  app docker-compose build
[+] Building 41.0s (8/8) FINISHED                                               
 => [web internal] load build definition from Dockerfile                   0.0s
 => => transferring dockerfile: 441B                                       0.0s
 => [web internal] load .dockerignore                                      0.0s
 => => transferring context: 2B                                            0.0s
 => [web internal] load metadata for docker.io/library/ruby:3.1.2-alpine3  5.4s
 => [web 1/4] FROM docker.io/library/ruby:3.1.2-alpine3.15@sha256:8a8cba1  6.5s
 => => resolve docker.io/library/ruby:3.1.2-alpine3.15@sha256:8a8cba16117  0.0s
 => => sha256:9621f1afde84053b2f9b6ff34fc7f7460712247c01c 2.82MB / 2.82MB  0.9s
 => => sha256:21fafba7b6adda1d48ec07e3047626a3c4f4340db51 3.69MB / 3.69MB  0.7s
 => => sha256:33a3439ad63dfb217b7df7acc43ed7bca7e070f0b3dbef3 223B / 223B  0.7s
 => => sha256:8a8cba161179095feea42c585711f3a02624737b0ed 1.65kB / 1.65kB  0.0s
 => => sha256:f7d8374ee7b8c959ed6334101c366c1e491037c6e4a 1.36kB / 1.36kB  0.0s
 => => sha256:5f7763e7849218c95267172c4dd712142aa0e588c35 6.10kB / 6.10kB  0.0s
 => => sha256:e14d080185efc14b694993705b4c2aa793338873e 29.53MB / 29.53MB  3.9s
 => => sha256:caae273acc14b6604dd1272fdc3ec3a136e2a1f0a496d4e 173B / 173B  1.3s
 => => extracting sha256:9621f1afde84053b2f9b6ff34fc7f7460712247c01cbab48  0.1s
 => => extracting sha256:21fafba7b6adda1d48ec07e3047626a3c4f4340db51965fa  0.6s
 => => extracting sha256:33a3439ad63dfb217b7df7acc43ed7bca7e070f0b3dbef39  0.0s
 => => extracting sha256:e14d080185efc14b694993705b4c2aa793338873e156e253  2.2s
 => => extracting sha256:caae273acc14b6604dd1272fdc3ec3a136e2a1f0a496d4ee  0.0s
 => [web 2/4] WORKDIR /app                                                 0.9s
 => [web 3/4] RUN apk update  && apk upgrade  && apk --update add         15.3s
 => [web 4/4] RUN gem update --system  && gem install bundler --no-docum  10.1s
 => [web] exporting to image                                               2.8s
 => => exporting layers                                                    2.8s
 => => writing image sha256:ede4cf3d48344c8fe15998ab3d6f20375d55c9ad69cf5  0.0s
 => => naming to docker.io/library/app-web                                 0.0s
Gemfile
Gemfile
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.1.2"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", ">= 7.0.8"

docker-compose run --rm web bundle exec rails new . --database=postgresqlで新規Railsプロジェクトを作成します。
DBの情報を追加します。

config/database.yml
config/database.yml
username: <%= ENV.fetch('POSTGRES_USER') { 'user' } %>
password: <%= ENV.fetch('POSTGRES_PASSWORD') { 'password' } %>
host: <%= ENV.fetch('DB_HOST') { 'db' } %>
port: <%= ENV.fetch('DB_PORT') { 5432 } %>

docker-compose run --rm web bundle exec rails db:createでDBを作成します。

DB
➜  app git:(main) docker-compose run --rm  web bundle exec rails db:create
[+] Building 0.0s (0/0)                                                         
[+] Creating 1/0
 ✔ Container app-db-1  Running                                             0.0s 
[+] Building 0.0s (0/0)                                                         
Created database 'app_development'
Created database 'app_test'

docker-compose upでサーバーを立ち上げます。

graphqlをインストールする

Gemfileにこちらのgemを追加し、インストールします。

Gemfile
group :development do
  gem "graphiql-rails"
end

gem "faker"
gem "graphql"

graphql-railsは、RailsアプリでGraphQLを実装するためのGemの1つです。
このGemは、Railsアプリ内でGraphQL APIを簡単にセットアップし、管理することができます。
GraphQLスキーマの定義、クエリの処理、リゾルバの作成などのタスクをサポートします。また、Railsのルーティングやコントローラーと統合して、GraphQLエンドポイントを提供することも可能です。

graphiqlは、GraphQLのクエリやミューテーションをテストおよびデバッグするためのWebベースのIDE(統合開発環境)です。
graphiqlを使用すると、ブラウザ上でGraphQLスキーマを検索し、クエリを作成し、実行して結果を視覚化することができます。これにより、GraphQLエンドポイントを直接ブラウザでテストすることが可能になります。graphiqlは、開発中やデバッグ時に便利です。

graphql-railsgraphiqlは、組み合わせて使用することで、Railsアプリ内で簡単にGraphQL APIを構築し、開発やデバッグの効率を向上させることができます。

https://github.com/graphql/graphiql
https://github.com/rmosolgo/graphiql-rails

インストーラーを実行する
ターミナル
rails generate graphql:install
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/app_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"
       route  graphiql-rails
      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/app_schema.rb

rails generate graphql:installコマンドは、RailsアプリにGraphQLの基本構造をセットアップするためのジェネレーターです。このコマンドを実行すると、以下の作業が行われます。

  1. GraphQLの設定ファイルが作成されます。
  2. GraphQLのルートクエリ(Query)とミューテーション(Mutation)のディレクトリが作成されます。
  3. GraphQLのスキーマファイルが作成され、ルートクエリとミューテーションのエントリーポイントが定義されます。
  4. GraphQLのコントローラーが作成され、GraphQLのエンドポイントがマウントされます。

モデルを作成する

ユーザー、投稿、コメントモデルを作成します。

rails g model user name email
rails g model post user:references title content
rails g model comment user:references body post:references
rails db:migrate

モデルの関連付けを設定します。

app/models
class User < ApplicationRecord
    has_many :posts, dependent: :destroy
    has_many :comments, dependent: :destroy
end

class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
end

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

fakerを使ってユーザーと投稿を作成します。

db/seeds.rb
10.times do
    user = User.create(
        email: Faker::Internet.email,
        name: Faker::Name.name
    )

    Post.create(
        user: user,
        title: Faker::Movie.title,
        content: Faker::Movie.quote
    )
end

rails db:seedを実行します。

typeを作成する

GraphQLにおけるType(タイプ)は、GraphQLスキーマで使用される基本的な概念の一つです。Typeは、GraphQL APIがクライアントに公開するデータの型や構造を定義します。
データ構造を定義するために使用されるので、Typeは通常、スキーマ内で多くの場所で再利用されます。

主なTypeには以下のようなものがあります。
  1. Scalar Type(スカラータイプ): スカラータイプは、単一の値を表すデータ型です。例えば、文字列、数値、真偽値、日付などがあります。代表的なスカラータイプには、StringIntFloatBooleanIDなどがあります。

  2. Object Type(オブジェクトタイプ): オブジェクトタイプは、複数のフィールドを持つ構造化されたデータ型です。オブジェクトタイプは、フィールドとその型を定義します。オブジェクトのフィールドには、他のType(スカラー、オブジェクト、リストなど)も含まれます。

  3. List Type(リストタイプ): リストタイプは、複数の要素を持つ配列やリストを表します。リスト内の各要素は、指定された型のデータを持ちます。

  4. NonNull Type(非nullタイプ): 非nullタイプは、その値がnullでないことを示します。GraphQLスキーマ内でフィールドや引数に!を付けることで、非nullタイプを定義することができます。

https://graphql.org/learn/schema/#object-types-and-fields

作成したユーザーモデルのフィールドとその型を定義します。

rails generate graphql:object user
      create  app/graphql/types/user_type.rb
app/graphql/types/user_type.rb
# frozen_string_literal: true

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String
    field :email, String
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

rails generate graphql:object userコマンドは、Railsアプリケーション内に新しいGraphQLのオブジェクトタイプを作成するためのジェネレーターです。
UserTypeという名前のGraphQLオブジェクトタイプを生成されます。その中にユーザーモデルのフィールドやリレーションシップが定義されます。

ポストとコメントも同じように作成していきます。

rails generate graphql:object post
      create  app/graphql/types/post_type.rb
rails generate graphql:object comment
      create  app/graphql/types/comment_type.rb

GraphQLではカスタムフィールドを定義することができます。カスタムフィールドは、既存のモデルやオブジェクトの属性やリレーションシップに依存せず、任意の処理やデータを提供するために使用されます。

たとえば、特定の条件に基づいてユーザーのリストをフィルタリングしたり、複数のモデルからデータを結合して返したりするカスタムフィールドを定義することができます。

カスタムフィールドを定義するには、GraphQLのオブジェクトタイプにメソッドを追加する方法があります。これにより、リゾルバ内でカスタムロジックを処理できます。

GraphQLのUserオブジェクトタイプにカスタムフィールドを追加してみます。

# app/graphql/types/user_type.rb
module Types
  class UserType < BaseObject
    # カスタムフィールド
    field :post_count, Integer, null: false
    field :posts, [Types::PostType], null: false

    # ここにカスタムフィールドのロジックを記述
    # たとえば、特定の条件に基づいてデータを取得したり、計算したりする
    def post_count
      object.posts.size
    end
  end
end

UserTypepost_countというカスタムフィールドが追加されています。このフィールドは、String型を返し、null値を許容しません。
post_countメソッド内で、ユーザーの投稿数のカスタムロジックを実装し、データを返すことができます。
postsはモデルの関連付けにて定義されているため再定義する必要がないです。

QueryTypeを定義する

QueryTypeは、GraphQLスキーマのエントリーポイントであり、クライアントがクエリを実行してサーバーからデータを取得するために使用するメソッドを定義します。
例えば、getUsergetAllUsersなどのフィールドを含むことができます。
QueryTypeは通常、Typeのフィールドを含み、クライアントが取得できるデータを指定します。

app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    # 全てのユーザーを取得する
    field :users, [Types::UserType], null: false
    def users
      User.all
    end

    # IDからユーザーを取得する
    field :user, Types::UserType, null: false do
      argument :id, ID, required: true
    end
    def user(id:)
      User.find(id)
    end
  end
end

このように、QueryTypeを定義することで、クライアントはGraphQLスキーマ内のエントリーポイントを使用してデータを取得できます。

検証

http://localhost:3000/graphiqlにアクセスします。
GraphQLがブラウザに表示されます。

ユーザーとユーザー毎の投稿数を取得してみます。

個別のユーザーを取得してみます。

終わり

graphQLのセットアップとデータの取得についてまとめてみました。
次回はデータを変更するための操作mutationについてもまとめていきたいと思います。

https://graphql-ruby.org/getting_started
https://rails-graphql.dev/

Discussion