はじめに
GraphQL APIサーバーをRailsで構築する方法です。rails db:create
~データ取得までをやりたいと思います。resolver
やmutation
を使えばデータのフィルタリング・作成・更新も実装できますが、記事が長くなるので、別でまとめようと思います。
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_name
はname
の末尾に「さん」を追加しただけのフィールドです。
実際のコードはこうなります。
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が用意しているString
やFloat
などの型と違って、posts
は自分達で生成したPost Typeの配列が格納されるフィールドです。
なので、先ほど生成したPostTypeを配列の[]
で囲んだ形の型を指定することになります。field
の第2引数に指定できる型はgemが用意している型か、自分で生成した型のみです。
実際に呼び出す時にどうなるかは後の方でGraphiQLというツールを用いて説明いたします。
今はこんな感じで型をカスタマイズできるよ程度に理解していただければOKです。
他のTypeにもアソシエーションは追加しておきましょう。
Post Typeにはtags
とuser
を、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
にアクセスしてみましょう。
以下のような画面が表示されたら成功です!
左の白い部分にクエリを入力し実行し、右の灰色部分にデータが表示される仕組みです。
左のパネルに以下のクエリを書いて、実行ボタン(▶︎)を押してください。
{
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
問題が発生していることがわかるかと思います。
N+1
を解決するgemもいくつかありますので、別記事でそれも紹介したいと思います。
その前にGraphQLの機能であるresolver
, mutation
についても記事を書きたいと思ってるので覗いていただけると嬉しいです!