遅くなりましたが、 PORT Advent Calendar 12 日目 13日目になってしまった 担当の 18 卒の山内です。
PORT 株式会社で、サーバーサイドエンジニアとして開発を行っています。
うまくまとまっていないので、追加・修正していく予定です。
間違っているところがあれば、 コメント
もしくは 編集リクエスト
をお願いします。
なぜ GraphQL について書くか
入社してすぐ、新卒エンジニアで集まって行った LT 会で GraphQL を少し触った内容を話したのですが、それからなかなか手を付けられてなかったので、今回のアドベントカレンダーをきっかけにもう一度挑戦しようと思いました。
GraphQL とは
- API への問合せ言語 (クエリ言語)
- Facebook 製
- 1 回のリクエストで必要なものだけ取得、更新できる
- エンドポイントが 1 つ
- 破壊的な変更が起こらないので、
v1
やv2
といったバージョンを付けなくて良い -
REST API
の代わり?
Query
- データ取得
-
REST API
のGET
相当のもの
Mutation
- データ更新
-
REST API
のPOST
,PUT
,DELETE
相当のもの
Subscription
- サーバでのデータの変化をクライアントに通知?
どこで使われてるの
- GitHub
既存のプロジェクトに GraphQL を入れる
graphql-ruby はただの GraphQL 処理系で、ウェブアプリケーション全体に影響を及ぼすものではありません。Rails 的に言えば、
GraphqlController#execute
というアクションを追加するだけですし、これすらrails generate graphql:install
で雛形を生成できます。したがって、すでに RESTful API で運用している Rails アプリにも GraphQL API を足すことは簡単ですし、それから徐々に移行していけばいいのです。
参考: 既存の Rails アプリに GraphQL API を足す - Notes on GraphQL for Ruby
と書かれているとおり、追加しても既存のプロジェクトには何の影響も与えないので安心して追加できそうです。
gem
のインストール
まず、初めに GraphQL
の gem
をインストールします。
gem 'graphql'
$ bundle install
次に、 GraphQL
のセットアップをしましょう。
$ bundle exec rails g graphql:install
Running via Spring preloader in process 33086
create app/graphql/types
create app/graphql/types/.keep
create app/graphql/sample_app_schema.rb
create app/graphql/types/base_object.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/types/query_type.rb
add_root_type query
create app/graphql/mutations
create app/graphql/mutations/.keep
create app/graphql/types/mutation_type.rb
add_root_type mutation
create app/controllers/graphql_controller.rb
route post "/graphql", to: "graphql#execute"
gemfile graphiql-rails
route graphiql-rails
Gemfile has been modified, make sure you `bundle install`
GraphiQL
grahiql-rails
という gem
を追加することで動作確認を簡単に行なえます。
$ bundle exec rails g graphql:install
した際に、 graphiql-rails
が Gemfile
に追加されているので、
$ bundle install
ここで、今回利用した gem のバージョンを確認しておきます。
$ bundle exec gem list graph
*** LOCAL GEMS ***
graphiql-rails (1.5.0)
graphql (1.8.11)
$ bundle exec rails g graphql:install
を実行した際に、 graphiql
用のルーティングも追加されているので、
Rails.application.routes.draw do
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
post "/graphql", to: "graphql#execute"
~~
end
サーバーを立ち上げて、 localhost:3000/graphiql
にアクセスしましょう。
アクセスすると、以下のような画面が表示されます。
この画面で実装後、すぐに動作確認できます。
また、右上の Docs
でドキュメントの確認もできます。
すでに、テスト用の test_field
が定義されているので実行してみましょう。
module Types
class QueryType < Types::BaseObject
# 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
end
end
下記のような Query
を投げることで、 "Hello World!"
が返って来ました。
Query
次は、自分が欲しい情報を得られる Query
を投げられるようにしていきましょう。
これから説明する中で、
ShopType
やBrandType
などを定義します。
すでに、既存の Rails アプリのActiveRecord
のモデルとしてShop
、Brand
が作成された状態だと仮定して進めるので、それぞれの Rails アプリのモデルに合わせて読み進めてください。
まず、object
を作成します。
$ rails g graphql:object Shop id:ID! name:String! access:String!
上記コマンドを実行すると app/graphql/types/shop_type.rb
が作成されます。
id:ID!
のようにすると null: false
に、 name:String
のように !
を付けない場合は null: true
になります。
module Types
class ShopType < Types::BaseObject
description '店舗'
field :id, ID, '店舗ID', null: false
field :name, String, '店舗名', null: false
field :access, String, 'アクセス', null: true
field :open_time, String, null: false
field :close_time, String, null: false
end
end
null: true
、 null: false
は Rails のモデルのカラムが null
を許容するかどうかに合わせると良いでしょう。
また、次の SampleType
の field
のように, 説明を書くと Docs
に追加されます。
module Types
class SampleType < Types::BaseObject
description '説明説明'
field :field_name1, '説明1', null: false
field :field_name2, null: false, description: '説明2'
end
end
さきほど作成した ShopType
を使って Query
で呼び出せるように QueryType
に shop
を定義します。
argument
には、引数として利用したいものを定義します。今回は 1 つですが、複数定義できます。
module Types
class QueryType < Types::BaseObject
field :shop, ShopType, null: true do
description '店舗をidで検索'
argument :id, ID, '店舗ID', required: true
end
def shop(id:)
Shop.find(id)
end
end
end
以下のような Query
を投げると対応した JSON
が返ってきます。
id
を引数として渡さない場合は、最後の Shop を返すようにしてみました。
module Types
class QueryType < Types::BaseObject
field :shop, ShopType, null: true do
description '店舗をidで検索'
argument :id, ID, '店舗ID', required: false # id がなくても検索できるように
end
def shop(id: nil) # id を渡さないときには nil を渡す
id ? Shop.find(id) : Shop.last # idがなければ最後の Shop を返す
end
end
end
型
field
の型は以下のものから指定できます。
ID, String, Fixnum, Integer, Float, Date, Time, DateTime, Array, Object, Hash
Date, Time, DateTime は、前回触ったときは使えませんでした。
今後、試してみます。
自分で定義することも可能です。
参考 graphql-rails/lib/graphql/rails/types.rb
BrandType
も ShopType
と同じ様に作成してみました。
$ rails g graphql:object Brand id:ID! name:String!
ちなみに、Rails のモデルやコントローラーと同じ様に rails d
で消せます
$ rails d graphql:object Brand id:ID! name:String!
module Types
class BrandType < Types::BaseObject
description 'ブランド'
field :id, ID, 'ブランドID', null: false
field :name, String, 'ブランド名', null: false
end
end
1:1
のアソシエーションの場合
Shop と Brand が 1:1
のときに、
class Shop < ApplicationRecord
belongs_to :brand, required: true
has_many :reviews, dependent: :destroy
~~
end
class Brand < ApplicationRecord
has_many :shops, dependent: :destroy
has_many :reviews, through: :shops, dependent: :destroy
~~
end
ShopType
の field
の型を BrandType
とすると、
module Types
class ShopType < Types::BaseObject
description '店舗'
~~
field :brand, BrandType, 'ブランド', null: false
end
end
次のように、呼び出せます。
1:N
のアソシエーションの場合
Shop と Review が 1:N
のとき、
class Shop < ApplicationRecord
~~
has_many :reviews, dependent: :destroy
~~
end
class Review < ApplicationRecord
~~
belongs_to :shop
~~
end
$ be rails g graphql:object Review
module Types
class ReviewType < Types::BaseObject
description 'レビュー'
field :id, ID, 'レビューID', null: false
field :reviewer_name, String, null: false
field :shop, ShopType, '店舗', null: false
end
end
ShopType
の field
の型を [ReviewType]
とすると、
module Types
class ShopType < Types::BaseObject
description '店舗'
~~
field :reviews, [ReviewType], 'レビュー', null: false
end
end
次のように呼び出せます。
Shop を店舗名であいまい検索してみる
module Types
class QueryType < Types::BaseObject
~~
field :shops, [ShopType], null: true do
description '店舗名であいまい検索'
argument :name, String, '店舗名', required: true
end
def shops(name:)
Shop.where('name LIKE ?', "%#{name}%").limit(10)
end
~~
end
end
Shop モデルに定義されているインスタンスメソッドを呼んでみる
class Shop < ApplicationRecord
~~
# 全レビューの総合点平均
def overall_score
~~
end
end
method
も field
に定義すれば Query
で呼ぶことができます。
module Types
class ShopType < Types::BaseObject
description '店舗'
~~
field :overall_score, Float, '全レビューの総合点平均', null: true, method: :overall_score
end
end
スネークケースで定義した field
は、GraphQL
ではキャメルケースで呼ぶようになっていました。
overall_score
は overallScore
になります。
一度に複数の情報を取得
以下のように、同時に複数の情報を取得できます。
Mutation
前のバーションの情報が多くて、よくわからなかったので今後調べて書きます。
まとめ
- 楽しい
- 結構簡単に実装できる
- 以前触ったときからの変更が多い
-
description
を書いておくと勝手にDocs
に追加される
参考
- GraphQL - Welcome
- Notes on GraphQL for Ruby
- 雑に始める GraphQL Ruby【class-based API】
- GraphQL Ruby の使い方 (基礎編)
- GraphQL を最速でマスターするための意識改革3ヶ条
よくわかっていないところ
わかり次第追記します。
-
Mutation
-
app/graphql/types/query_type.rb
が増えてきたら読みづらそう- 分割すれば良い
-
この辺
app/graphql/types/base_enum.rb
app/graphql/types/base_input_object.rb
app/graphql/types/base_interface.rb
app/graphql/types/base_eobject.rb
app/graphql/types/base_scaler.rb
app/graphql/types/base_union.rb
-
$ rails g graphql:xxxx
-
N+1
問題がよく起きるらしい -
client
- apollo
最後に
PORT 株式会社では、自社サービスを支えてくれる優秀な Ruby エンジニアを募集しています(Ruby エンジニア以外も)。
もくもく会も行なっていますので、ぜひ一緒にもくもくしましょう!
PORT Advent Calendar はまだまだ続きます。乞うご期待!