目次
API、好きですか?!
データイノベーション部 AI-labo所属の吉岡です。
私は今入社3年目で、入社当初からRuby on Railsを使っていました。
入社2年目から配属してるプロジェクトでは、Rails + GraphQLでバックエンド開発をしております。
過去に自分は趣味で色んなWebAPIを触ってきましたが、GraphQLはすごく画期的です。
Railsに慣れたエンジニアがWebAPIを開発しようと思ったとき、GraphQLを使うとこういう利点があるよ!というのを伝えたいと思います。
こういう人に読んでもらいたい!
主に以下に当てはまる人は、より楽しめる記事になっているかと思います!
- Ruby on Railsのフロント・バックエンドの開発をした経験があり、controllerやmodelの概念がある程度わかっている人
- Web APIやREST APIについてある程度どんなものかわかっている人
- バックエンドをAPIにして、フロントエンドをReactやVueなどを使ってスマホアプリを作ってみたい人
GraphQLについて
GraphQLとは、Facebook社が開発したAPI向けのクエリ言語です。
……といっても私自身が言葉的にそんなに深く理解しているわけではないので、実際何ができるかを中心に説明して行きたいと思います。
例えばこういうことができる
以下のようなカラム構成のテーブルがあると想定します。
users テーブル
- id
- name
- created_at
- updated_at
articles テーブル
- id
- user_id
- body
- created_at
- updated_at
comments テーブル
- id
- article_id
- user_id
- text
- created_at
- updated_at
もし、あるページであるuserのname、そのユーザに紐づいたarticlesのbodyとcreated_at、さらにそのarcitlesに紐づいたcommentsのtextを取るページがあったとき、どのようにAPIを実装・取得しますか?
この条件で取得できるようなAPIを、例えばRESTで実装するとき、いくつか懸念点が出てくると思います。
- users、articles、commentsを取る時にそれぞれリクエストを投げるか?
- 1回のリクエストをするとして、そのためのエンドポイントを新たに作らなければいけないのか?
- articlesやcommentsの全カラムを取得するAPIはあるが、使わないカラムのデータも受け取らなければいけないのか?
- もしこのページで他のデータが必要になったときに、どう対応するか?
Railsでフロント込みの開発に慣れていると「N+1にならないようにしなきゃ……」くらいの懸念点になるかと思います。
しかしフロントエンドを別物として開発する場合、フロント側にはmodelという概念はないため、APIにする場合は上記のようなことも重要になってきます。
そんなとき、GraphQLは以下のようなqueryを書けば1発で解決できちゃうんです。
query {
user(id: 1){
articles{
body
createdAt
comments{
text
}
}
}
}
「えっそんな簡単にできるの?」「実際どう投げるのか教えてもらわないとよくわかんない」「queryって何?」っていう人の為に、もう少し詳細に説明したいと思います!
どう使うの?
APIを使う上で最初に知りたいのってやはりエンドポイントですよね。
GraphQLのエンドポイントは、以下のようになります。
〇〇.com/graphql
「え?これだけ?これで情報どうやって取るの?」って思うかもしれません。
実は、やってる内容的にはGETなのですが、GraphQLの場合は全部POST形式で投げています。
1つのエンドポイントは固定で、リクエストbodyに上記のqueryを入れてPOSTで投げます。
リクエストに含まれるqueryをrails側で解釈し、queryに指定されている内容をレスポンスするという形です。
メリット
ここまで軽くGraphQLについて説明してきましたが、上のことを踏まえると以下のようなメリットが見えてきます。
- エンドポイントが1つで済む。
- 欲しい情報だけを取得することができる。
- テーブル構成が複雑な場合でも1回のリクエストで様々な情報を取りきることができる。
中でもエンドポイントが1つで済むのは一番の利点だと思っていて、バックエンド側から返す情報が変更もしくは新しく増えたときでも、フロントエンドではGraphQLのqueryの記述を変更すれば良いだけになります。
ローカル環境・本番環境での確認に役立つツールがあって便利!
GraphQLには、ローカル環境での確認に便利な GraphiQL
という機能があります。
これはローカルでサーバを立ち上げ、 localhost:3000/graphiql/
にアクセスすると、queryを試せるという機能になっています。(urlは初期設定で、変更もできます。)
また、chromeの拡張機能に Altair GraphQL Client
というものがあります。
こちらはGraphiQLと同じ機能を本番環境等で実際に試すことができます。
また、Altairではリクエストヘッダの設定もできますので、ヘッダの情報が必要な場合にも対応できます!
RailsでGraphQLを動かしてみる!!
GraphQLが便利なことはわかった……けど、実装が難しかったら使いたくないですよね。
Railsに触れてたユーザがGraphQLを使用するのに必要なことは以下の4つです。
- gemが追加できること
- controllerを作成して、ルーティングできること
- modelが作れること
- hashが書けること
はい、これだけでできます。
Rails チュートリアルで学習する内容さえあれば完ペキにAPIを作ることができます。
1. まずは環境構築する
どのような形でも良いので、まずはRailsのアプリケーションを立ち上げます。
gemさえ入れれば既存のRailsプロジェクトでも動きますので、とりあえず立ち上げについては省略します。rails s
が動作する状態にしてから以下の手順を実行してみてください。
Railsのアプリケーションのディレクトリに入り、Gemfileに以下を追加します。
gem 'graphql' # <- 追加
group :development do
gem 'graphiql-rails' # <- developmentのgroup内に追加
end
インストールを実行します。
bundle install
bundle exec rails g graphql:install
そうすると以下が追加されているかと思います
app/graphql
├── {アプリケーション名}_schema.rb
├── mutations
└── types
├── mutation_type.rb
└── query_type.rb
また、config/rountes.rb
に以下のような記述が追加されているかと思います
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
post "/graphql", to: "graphql#execute"
これでGraphQLを使う準備が出来ました!
試しにrails sを立ち上げ、 http://localhost:3000/graphiql
を立ち上げると先ほどのGraphiQLの画面が出てくるかと思いますので、以下のqueryを記述して実行してみてください。
{
testField
}
以下のようなレスポンスが返ってきたら成功です!
{
"data": {
"testField": "Hello World!"
}
}
エラー注意!
rails・graphqlのバージョンによっては app/assets/config/manifest.js
を以下のように修正する必要があります。
もし Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show
のようなエラーが出たら修正してみてください。
//= link graphiql/rails/application.css
//= link graphiql/rails/application.js
2. queryの構造についての説明
早速queryを書いていきますが、先にqueryについての説明をします。
queryは基本以下の構造になります。
query queryの名前 {
fieldName1
fieldName2(arg: argument)
fieldName3 {
fieldName3-1
fieldName3-2
}
}
- query: queryかmutationを指定します。
- query: 情報を取得するためのもの
- mutation: 情報を更新するためのもの(本稿では説明しません)
- queryの名前: queryを書く人(フロント開発者など)が分かりやすくするためにつけるもの
- fieldName: 何の情報を取るかを指定する(階層構造にできる)
- arg: 引数指定ができます。
注意!
以下でrailsから定義していきますが、rubyファイルでの定義はスネークケース、queryの記述ではローワーキャメルケース(先頭だけ小文字のキャメルケース)で書きます!
ごっちゃにならないように注意してください!
筆者もよくqueryをスネークケースで書いてエラーに苦しんだりしてます。
エラーが出たら、まずは書き方を疑ってみてください。
3. とりあえず何かを返すqueryを作る
とりあえずGraphQLを導入したので、まずは簡単なqueryを作成してみたいと思います。
以下のような情報を返したいとします。
query todayInfo {
currentDate # 今日の日付
todayWeather { # 今日の天気
weather # 天気
temperature # 温度
}
}
先ほど作成された app/graphql/types/query_type.rb
に以下の行を追加します。
module Types
class QueryType < Types::BaseObject
### (省略) ###
##### ここから追加
field :current_date, String, null: false, description: '今日の日付'
def current_date
Date.today.strftime("%Y年 %m月 %d日")
end
field :today_weather, WeatherType, null: false, description: '今日の天気'
def today_weather
{ weather: '晴れ', temperature: 60 }
end
##### ここまで追加
end
end
そして、 app/graphql/types/weather_type.rb
を作成し、以下のコードを書いてください(丸コピで大丈夫です!)
module Types
class WeatherType < Types::BaseObject
field :weather, String, null: false, description: '天気'
def weather
object[:weather]
end
field :temperature, Int, null: false, description: '温度'
def temperature
object[:temperature]
end
end
end
上記のコードの解説です!
まず以下の1行でfieldの定義をします。
field :query名, 型, null: nullを許可するか, description: 'クエリの説明'
そして、query名と同じ名前で定義されたメソッドを リゾルバ(resolver)
といい、このメソッド内の内容を実際にqueryで返します。
def query名
返す値を記述
end
つまり、current_dateは以下のように解釈されます
- field名はcurrent_date(GraphQL上では
currentDate
) - String型
- nullは許可していない
- 返ってくる値は
Date.today.strftime("%Y年 %m月 %d日")
になる
続いて、todayWeather
の解説です。
最初の方でも述べましたが、GraphQLは階層構造で返すことができます。
これを実現するためには、fieldの中にfieldがあって……という構造にする必要があります。
先ほど、型はStringで指定していましたが、自分で作った型を指定することもできます。
以下の1行ではWeatherTypeを指定しています。
field :today_weather, WeatherType, null: false, description: '今日の天気'
WeatherTypeは weather
, temperature
という2つのfieldがありますので、 todayWeather
のfieldでは weather
, temperature
という情報を取ることができます。
また、作った型を指定したときは、リゾルバの内容が型に渡されます
field :today_weather, WeatherType, null: false, description: '今日の天気'
def today_weather
{ weather: '晴れ', temperature: 60 } # <- この値がWeatherTypeに渡される
end
渡されたリゾルバの内容は、 object
という変数に入っています。
field :weather, String, null: false, description: '天気'
def weather
object[:weather] # <- objectの中身は { weather: '晴れ', temperature: 60 }
end
この状態でrails sでサーバを立ち上げてGraphiQL( http://localhost:3000/graphiql
)にアクセスします。
左側に以下の記述をして、画面上部の実行ボタン(右向き三角のボタン)を押してください。
query todayInfo {
currentDate # 今日の日付
todayWeather { # 今日の天気
weather # 天気
temperature # 温度
}
}
そうすると以下のようなレスポンスが返ってきます。
{
"data": {
"currentDate": "2020年 09月 30日",
"todayWeather": {
"weather": "晴れ",
"temperature": 60
}
}
}
これがGraphQLの基礎になります。
余裕があったら!
today_weatherのリゾルバ内の記述を変えてみてください!
def today_weather
{ weather: '雨', temperature: 10 } # <- この値がWeatherTypeに渡される
end
レスポンスが以下のように変わるはずです!
{
"data": {
"currentDate": "2020年 09月 30日",
"todayWeather": {
"weather": "雨",
"temperature": 10
}
}
}
4. 引数と複数件の情報
情報を取るのに配列や引数が必要になってきます。
以下のようなqueryを想定しましょう
query monthInfo{
month(monthNum: 1) { # 月の情報(月指定で1件)
name # 英名
days # 日数
}
months { # 月の情報(全件)
name # 英名
days # 日数
}
}
こちらのqueryは、現在日と月の英名およびその月の日数を返す想定です。
先ほど作成された app/graphql/types/query_type.rb
に以下の行を追加します。
module Types
class QueryType < Types::BaseObject
### (省略) ###
##### ここから追加
field :month, MonthType, null: false, description: '月の情報(月を指定して1件)' do
argument :month_num, Int, '月の数字', required: true
end
def month(month_num:)
month_hash_array[month_num - 1]
end
field :months, [MonthType], null: :false, description: '月の情報(全件)'
def months
month_hash_array
end
# 返すためのデータ
def month_hash_array
[
{ name: 'January', days: 31 },
{ name: 'February', days: 28 },
{ name: 'March', days: 31 },
{ name: 'April', days: 30 },
{ name: 'May', days: 31 },
{ name: 'June', days: 30 },
{ name: 'July', days: 31 },
{ name: 'August', days: 31 },
{ name: 'September', days: 30 },
{ name: 'October', days: 31 },
{ name: 'November', days: 30 },
{ name: 'December', days: 31 }
]
end
##### ここまで追加
end
end
そして、 app/graphql/types/month_type.rb
を作成し、以下のコードを書いてください(こちらも丸コピで大丈夫です!)
module Types
class MonthType < Types::BaseObject
field :name, String, null: false, description: '英名'
def name
object[:name]
end
field :days, Int, null: false, description: '日数'
def days
object[:days]
end
end
end
引数の付け方
まずは引数の付け方の解説です。
fieldに augument
を定義します。(ブロック内で指定しています。)
field :month, MonthType, null: false, description: '月の情報(月を指定して1件)' do
argument :month_num, Int, '月の数字', required: false
end
引数の指定については以下のようになっています。
Stringを指定すれば文字列にできますし、引数用の型を定義すれば複雑な引数指定もできます。(本稿では省略します。)
argument :引数名, 型, '説明', required: 必須にするか
続いてリゾルバについてです。
引数を指定したfieldについてはキーワード引数としてリゾルバを定義すれば、引数の内容が取得できます。
(※ 1月と指定した場合、1月の情報はmonth_hash_array[0]にあるので、 month_num - 1
になっています。)
def month(month_num:)
month_hash_array[month_num - 1]
end
配列を返す
続いて配列の返し方です。
複数件のレスポンスになるfieldは、以下のように型指定時に []
で囲むだけで、配列として返すことができます。
field :months, [MonthType], null: :false, description: '月の情報(全件)'
def months
month_hash_array
end
上記の例だと、 { name: '月名', days:
日数}
の塊が、いくつもある形のレスポンスができます。
また、全ての型を配列化ができます
例 )
- [‘いぬ’, ‘ねこ’, ‘アルパカ’] -> 型を
[String]
で指定する。 - [10, 23, 37] -> 型を
[Int]
で指定 - [{ weather: ‘晴れ’, temperature: 40 }, { weather: ‘雨’, temperature: 10 }] -> 型を
[WeatherType]
で指定( WeatherTypeについては前項を参照 )
実行してみる!
先ほどと同様にrails sでサーバを立ち上げてGraphiQL( http://localhost:3000/graphiql
)にアクセスします。
左側に以下の記述をして、画面上部の実行ボタン(右向き三角のボタン)を押してください。
query monthInfo{
month(monthNum: 1) { # 月の情報(月指定で1件)
name # 英名
days # 日数
}
months { # 月の情報(全件)
name # 英名
days # 日数
}
}
以下のようなレスポンスが返ってくると思います。
{
"data": {
"month": {
"name": "January",
"days": 31
},
"months": [
{
"name": "January",
"days": 31
},
{
"name": "February",
"days": 28
},
{
"name": "March",
"days": 31
},
{
"name": "April",
"days": 30
},
{
"name": "May",
"days": 31
},
{
"name": "June",
"days": 30
},
{
"name": "July",
"days": 31
},
{
"name": "August",
"days": 31
},
{
"name": "September",
"days": 30
},
{
"name": "October",
"days": 31
},
{
"name": "November",
"days": 30
},
{
"name": "December",
"days": 31
}
]
}
}
これでqueryはバッチリですね!
余裕があったら
引数を変えてみてください!
情報が変わります!
query monthInfo{
month(monthNum: 5) { # <- 変えてみる
name
days
}
months {
name
days
}
}
6. モデルの内容を返すqueryを書く
※ 本稿ではモデルについての詳細な説明は省略します。modelやデータは自分で作ってみてください!
例えば、以下のようなモデルがあったとします。
Userモデル(id以外はstring)
- id
- last_name
- first_name
- profile
- created_at
- updated_at
そして以下のようなqueryを作りたいとします。
query {
user(id: 1) { # Userモデルのデータ1件取得
full_name # last_name と first_nameを半角スペースで繋げたもの
profile # profileのデータをそのまま
created_at # created_atの年月日までの情報
}
users { # Userモデルのデータを全部取得
full_name # last_name と first_nameを半角スペースで繋げたもの
profile # profileのデータをそのまま
created_at # created_atの年月日までの情報
}
}
その場合、以下のようにTypeを定義します。app/graphql/types/query_type.rb
に以下を記述します。
module Types
class QueryType
### (省略) ###
##### ここから追加
field :user, UserType, null: false, description: 'ユーザ情報(id指定で1件)' do
argument :id, Int, 'ユーザid', required: true
end
def user(id:)
User.find_by(id: id)
end
field :users, [UserType], null: false, description: 'ユーザ情報(全件)'
def users
User.all
end
##### ここまで追加
end
end
app/graphql/types/user_type.rb
に以下を記述します。
module Types
class UserType
field :full_name, String, null: false, description: '姓名'
def full_name
object.last_name + ' ' + object.first_name
end
field :created_at, String, null: false, description: '作成日時'
def created_at
object.created_at.strftime("%Y年 %m月 %d日")
end
field :profile, String, null: false, description: 'プロフィール'
def profile
object.profile
end
end
end
解説!
先ほどはhashで指定しましたが、リゾルバから型に送るもの(object)はhashでもインスタンスでもActiveRecord::Relationでもいけます!
以下の2つのリゾルバを見てください。
このように指定すれば、1件の方には User.find_by(id: id)
したレコードを、全件の方には User.all
のRelationを入れることができます。
field :user, UserType ### 省略 ###
def user(id:)
User.find_by(id: id)
end
field :users, [UserType] ### 省略 ###
def users
User.all
end
Userインスタンスについても、objectに入ります。
なので以下のリゾルバではobjectの中にはUserのインスタンスが入っています。
field :full_name, String, null: false, description: '姓名'
def full_name
object.last_name + ' ' + object.first_name
end
実行してみよう!
以下のqueryを実行してみましょう!
成功したら、成功です!(細かいレスポンスは省略します)
query {
user(id: 1) { # Userモデルのデータ1件取得
full_name # last_name と first_nameを半角スペースで繋げたもの
profile # profileのデータをそのまま
created_at # created_atの年月日までの情報
}
users { # Userモデルのデータを全部取得
full_name # last_name と first_nameを半角スペースで繋げたもの
profile # profileのデータをそのまま
created_at # created_atの年月日までの情報
}
}
7. 便利な術!
GraphQLの記述を書くのに当たって便利な技を紹介します。
処理しなくて良いものはリゾルバが不要!
例えば先ほどのWeatherTypeをご覧ください。
module Types
class WeatherType < Types::BaseObject
field :weather, String, null: false, description: '天気'
def weather
object[:weather]
end
field :temperature, Int, null: false, description: '温度'
def temperature
object[:temperature]
end
end
end
weatherのfieldでは object[:weather]
を返していますが、hashまたはインスタンスで何も成形が不要なときはリゾルバの定義が不要になります。
なので、WeatherTypeは以下のように書き換えることができます。
module Types
class WeatherType < Types::BaseObject
field :weather, String, null: false, description: '天気'
field :temperature, Int, null: false, description: '温度'
end
end
スッキリしていいですね!
UserTypeいついても同様です!
module Types
class UserType
field :full_name, String, null: false, description: '姓名'
## 成形しているため、省略不可能
def full_name
object.last_name + ' ' + object.first_name
end
field :profile, String, null: false, description: 'プロフィール'
## 成形していないため、省略可能
def profile
object.profile
end
end
end
profileは何も成形していないので、以下のように書き換えることができます。
module Types
class UserType
field :full_name, String, null: false, description: '姓名'
## 成形しているため、省略不可能
def full_name
object.last_name + ' ' + object.first_name
end
field :profile, String, null: false, description: 'プロフィール'
end
end
ちなみに、 full_name
をUserモデルのインスタンスメソッドにした場合は object.full_name
という形にできるので、 full_name
のリゾルバも省略することができます!
module Types
class UserType
field :full_name, String, null: false, description: '姓名'
## full_nameをUserのインスタンスメソッドにした場合は、full_nameも省略可能
# def full_name
# object.fullname
# end
field :profile, String, null: false, description: 'プロフィール'
end
end
いいですね!!
Loaderを使って、N+1問題を解決!
最初に挙げたこのテーブル構造を思い出してください。
users テーブル
- id
- name
- created_at
- updated_at
articles テーブル
- id
- user_id
- body
- created_at
- updated_at
comments テーブル
- id
- article_id
- user_id
- text
- created_at
- updated_at
これを実装する場合、 user.articles
の中で article.comments
を普通に呼んでしまうと、N+1問題が発生します。
いくら1回のqueryで呼べたとしても、N+1が発生しては意味ないですよね?
そんなときにはLoaderを指定します。
loaderには association_loader.rb
というhas_manyの子レコードに対するloaderと record_loader
というbelongs_toの親レコードに対するloaderの2つが存在します。
本稿では詳しく説明しませんが、 graphql-batch
というgemを入れ、 graphql/loader/
配下にそれぞれのファイルを入れれば使えます。
複雑なテーブル構造で、データを呼びたいときは試してみてください。
これでもう最強ですね!
最後に
Railsを使ってきた人ならきっとgraphql-rubyを使いこなせると思います。
GraphQLは「AWS AppSync」というAWSのサービスでも採用されていて、主流なクエリ言語になりつつあると考えています!
RailsエンジニアでWebAPIやスマホやタブレットなどのアプリケーションを作りたいと考えている方、その知識を使って便利なAPIを作ってみませんか?