【Rails】GraphQL入門 セットアップ編

【Rails】graphql-ruby入門 セットアップ編 バックエンド

はじめに

GraphQL APIサーバーをRailsで構築する方法です。
rails db:create~データ取得までをやりたいと思います。
resolvermutationを使えばデータのフィルタリング・作成・更新も実装できますが、記事が長くなるので、別でまとめようと思います。

rails db:createまでのセットアップの仕方はこちらを参考にしてみてください!
ちなみにdocker-composeを使って作成していきますが、そこまで気にしなくても大丈夫だと思います。

GraphQLはどんな感じでデータ取得できるのか気になる方は、最後の方だけ見ていただけると参考になるかと思います!

GraphQLとは

軽く本当に軽くGraphQLについて触っておきます。
GraphQLは同じAPIエンドポイントを叩いているのに、HTTP bodyの書き方を変化させるだけで、欲しいデータを欲しいままに取得できるよっていうクエリ言語です。
( 公式ではクエリ言語って表現してますが、SQLほど難しくはないです。 )

なので、一度構築してしまえば、HTTP body( つまりはフロントエンドのコード )を変化させるだけで様々なデータを取得することができます。

GraphQLのメリット

僕が副業で入ってるプロジェクトでGraphQL採用しています。
そのプロジェクトはメンバーが2人で、僕は月数十時間しか稼働してないので、開発効率にとにかく重点を置いてます。

一般的なREST APIだと、仕様変更や画面追加のたびに、APIエンドポイントを追加することが割とあるかなと思います。
GraphQLだとその辺の融通がかなり効いて、一度構築してしまえば後はフロントエンドだけに集中できます。
また、HTTP bodyが取得データの操作をすると同時に、ドキュメントの役割も果たせるのもメリットです。

学習コストはそれなりにかかると思うので、正直、そこまで普及しない気はしてます。
ただ、個人などの少人数開発・仕様変更が半端ではない開発では試してみて損はないかと思います。

GraphQL 事前準備

モデル作成

以下の構成でテーブルを作りました。
User >- Post >-< Tag

マイグレーションファイルと実際のデータはこの記事で使ったものを流用してますので、詳しく知っときたい方は覗いてみてください。
( タイトルが気になった方も覗いてもらえると嬉しいです。)
railsでアソシエーション先がないデータを検索する方法

gemインストール

本記事では以下2つのgemを使用します。

  • graphql
  • graphiql-rails

以下をGemfileに追記してください。

gem 'graphql'
group :development do
  gem 'graphiql-rails'
end

追記したら以下コマンドを叩いてgemのインストールとgraphqlのセットアップを済ませましょう。

bundle install
rails g graphql:install

たくさんのファイルが生成されてログが出ていればOKです!
次からは実際にGraphQL APIを構築していきます。

Type作成

GraphQLのデータには型(Type)が決まっており、このステップでは型を定義していきます。
難しそうですが、はじめは型とモデルがイコールになるように作ってみようかと思います。
ただ、それだと不便になってくるので、このステップの最後の方で型を少しカスタマイズしてみましょう。

Type生成

gemが用意している型生成コマンドを打つだけです。
各モデル( 今回だとUser, Post, Tag )についてコマンドを打ちましょう。

# rails g graphql:object [モデル名]
rails g graphql:object User
rails g graphql:object Post
rails g graphql:object Tag

app/graphql/types/モデル名_type.rbを生成したと表示されたらOKです。
実際に各ファイルを覗いてみましょう。

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

こんな感じで既存のモデルを基にTypeを生成してくれています。
なので基本的な機能だけだったら特にソースコードをいじる必要はないです。

Typeにフィールドを追加する

gemが生成してくれたTypeだけでも十分APIとしては機能しますが、フィールドを追加してみましょう。
今回は、User Typeに新しく、honor_nameというフィールドを追加してみましょう。
honor_namenameの末尾に「さん」を追加しただけのフィールドです。

実際のコードはこうなります。

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

    field :honor_name, String, null: false

    def honor_name
      "#{object.name}さん"
    end
  end
end

新しくfield :honor_nameを追加し、フィールド名と同じ名前のメソッドも追加しています。
また、メソッド内でobjectを参照することで、自分自身にアクセスすることもできます。
これでUser Typeに新しくフィールドを追加することができました。

Typeにアソシエーションを追加する

先ほどフィールドを追加した方法と同じように、各モデルへのアソシエーションも追加してみましょう。
まずはUser Typeにpostsフィールドを追加してみましょう。

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

    field :honor_name, String, null: false
    field :posts, [PostType], null: false

    def honor_name
      "#{object.name}さん"
    end

    def posts
      object.posts
    end
  end
end

このように記載すれば他テーブルへのアソシエーションも実装できます。
注目していただきたいのはfield :postsの行です。
gemが用意しているStringFloatなどの型と違って、postsは自分達で生成したPost Typeの配列が格納されるフィールドです。
なので、先ほど生成したPostTypeを配列の[]で囲んだ形の型を指定することになります。
fieldの第2引数に指定できる型はgemが用意している型か、自分で生成した型のみです。

実際に呼び出す時にどうなるかは後の方でGraphiQLというツールを用いて説明いたします。
今はこんな感じで型をカスタマイズできるよ程度に理解していただければOKです。

他のTypeにもアソシエーションは追加しておきましょう。
Post Typeにはtagsuserを、Tag Typeにはpostsを追加します。

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :title, String
    field :user_id, Integer, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

    field :tags, [TagType], null: false
    # userは単数なので[]で囲まないです。
    field :user, UserType, null: false

    def tags
      object.tags
    end

    def user
      object.user
    end
  end
end
module Types
  class TagType < Types::BaseObject
    field :id, ID, null: false
    field :name, String
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

    field :posts, [PostType], null: false

    def posts
      object.posts
    end
  end
end

TypeをQueryとして登録する

作成してきたTypeをデータ取得の窓口として登録しましょう。
これさえやればデータ取得はできるようになります。

app/graphql/query_type.rbを以下のように変更しましょう

module Types
  class QueryType < Types::BaseObject
    # Add `node(id: ID!) and `nodes(ids: [ID!]!)`
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    # Add root-level fields here.
    # They will be entry points for queries on your schema.

    # TODO: remove me
    field :test_field, String, null: false,
      description: "An example field added by the generator"
    def test_field
      "Hello World!"
    end

    # ここから追記
    field :users, [Types::UserType], null: false
    field :posts, [Types::PostType], null: false
    field :tags, [Types::TagType], null: false

    def users
      User.all
    end

    def posts
      Post.all
    end

    def tags
      Tag.all
    end

  end
end

書き方はTypeにフィールドを追加した時と同じやり方です。

GraphiQL起動

GraphQL APIが構築できたので、GraphiQLというツールを使ってデータ取得をやってみましょう。

設定ファイル追記

GraphiQLを開くために、以下ファイルの編集・追加だけやっときましょう。
( ※apiモードだけの操作です。apiモードじゃない人はスキップしてもOKです。)

Rails.application.routes.draw do

  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
  end

  # ここから下はgemが追記してくれてた
  post "/graphql", to: "graphql#execute"
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  # root "articles#index"
end
require_relative "boot"

require "rails/all"
# 下1行を追記
require "sprockets/railtie"

Bundler.require(*Rails.groups)

module RailsGraphqlTest
  class Application < Rails::Application
    config.load_defaults 7.0
    # true -> falseに
    config.api_only = false
  end
end

ファイル追加

//= link graphiql/rails/application.css
//= link graphiql/rails/application.js

GrahpiQL操作

rails sでサーバーを立ち上げ、localhost:3000/graphiqlにアクセスしてみましょう。
以下のような画面が表示されたら成功です!

GraphiQL
GraphiQL

左の白い部分にクエリを入力し実行し、右の灰色部分にデータが表示される仕組みです。

左のパネルに以下のクエリを書いて、実行ボタン(▶︎)を押してください。

{
  users {
    id
    name
  }
}

右のパネルにこんな感じでユーザー一覧が出たら成功です。

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "食べる君"
      },
      {
        "id": "2",
        "name": "専門家の人"
      },
      {
        "id": "3",
        "name": "見る専マン"
      }
    ]
  }
}

次に、例えばユーザー一覧に加えて、そのユーザーが投稿した記事も付随して取得したいケースが出てきたとします。
その場合はクエリをこのように変えてみましょう。

{
  users {
    id
    name
    posts {
      id
      title
    }
  }
}

こんな感じのデータが右に表示されたら成功です!
( 長いので2人目のユーザー以降は省略してます。)

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "食べる君",
        "posts": [
          {
            "id": "9",
            "title": "チョコ食べてみた"
          },
          {
            "id": "10",
            "title": "アイス食べてみた"
          }
        ]
      },
      ......
    ]
  }
}

このように欲しいデータによってAPI実装を変化させるのではなく、クエリを変化させることで、要件変更に対応できるのがGraphQLのメリットです。
クエリの書き方に慣れないかもですが、色々データの構造を変えて取得したりしてみてください。

特定のユーザーが持つ記事を取得したり、特定のタグが付与された記事を投稿したユーザー一覧を取得したり、どんな構造になっても、APIを変化させる必要はありません。

クエリが冗長になってきたらTypeにフィールドを追加すればさらに便利になります。

問題点

今回、簡単にGraphQL APIでデータ取得を実装しましたが、railsのログを見ると問題点があります。
以下はユーザー全員とそれに紐づく記事を取得した時のログです。

N+1 problem

このようにN+1問題が発生していることがわかるかと思います。

N+1を解決するgemもいくつかありますので、別記事でそれも紹介したいと思います。
その前にGraphQLの機能であるresolver, mutationについても記事を書きたいと思ってるので覗いていただけると嬉しいです!

タイトルとURLをコピーしました