Posted at
PORTDay 12

既存のRailsプロジェクトにGraphQL APIを追加してみた

遅くなりましたが、 PORT Advent Calendar 12 日目 13日目になってしまった 担当の 18 卒の山内です。

PORT 株式会社で、サーバーサイドエンジニアとして開発を行っています。

うまくまとまっていないので、追加・修正していく予定です。

間違っているところがあれば、 コメント もしくは 編集リクエスト をお願いします。


なぜ GraphQL について書くか :writing_hand:

入社してすぐ、新卒エンジニアで集まって行った LT 会で GraphQL を少し触った内容を話したのですが、それからなかなか手を付けられてなかったので、今回のアドベントカレンダーをきっかけにもう一度挑戦しようと思いました。


GraphQL とは :question:


  • API への問合せ言語 (クエリ言語)

  • Facebook 製

  • 1 回のリクエストで必要なものだけ取得、更新できる

  • エンドポイントが 1 つ

  • 破壊的な変更が起こらないので、 v1v2 といったバージョンを付けなくて良い




  • REST API の代わり?


Query


  • データ取得


  • REST APIGET 相当のもの


Mutation


  • データ更新


  • REST APIPOST, PUT, DELETE 相当のもの


Subscription


  • サーバでのデータの変化をクライアントに通知?


どこで使われてるの :question:


既存のプロジェクトに 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 のインストール

まず、初めに GraphQLgem をインストールします。


Gemfile

gem 'graphql'



zsh

$ bundle install


次に、 GraphQL のセットアップをしましょう。


zsh

$ 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-railsGemfile に追加されているので、


zsh

$ bundle install


ここで、今回利用した gem のバージョンを確認しておきます。


zsh

$ bundle exec gem list graph

*** LOCAL GEMS ***

graphiql-rails (1.5.0)
graphql (1.8.11)


$ bundle exec rails g graphql:install を実行した際に、 graphiql 用のルーティングも追加されているので、


config/routes.rb

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 が定義されているので実行してみましょう。


app/graphql/types/query_type.rb

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 を投げられるようにしていきましょう。


これから説明する中で、 ShopTypeBrandType などを定義します。

すでに、既存の Rails アプリの ActiveRecord のモデルとして ShopBrand が作成された状態だと仮定して進めるので、それぞれの Rails アプリのモデルに合わせて読み進めてください。


まず、object を作成します。


zsh

$ 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 になります。


app/graphql/types/shop_type.rb

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: truenull: false は Rails のモデルのカラムが null を許容するかどうかに合わせると良いでしょう。

また、次の SampleTypefield のように, 説明を書くと Docs に追加されます。


app/graphql/types/sample_type.rb

module Types

class SampleType < Types::BaseObject
description '説明説明'

field :field_name1, '説明1', null: false
field :field_name2, null: false, description: '説明2'
end
end


さきほど作成した ShopType を使って Query で呼び出せるように QueryTypeshop を定義します。

argument には、引数として利用したいものを定義します。今回は 1 つですが、複数定義できます。


app/graphql/types/query_type.rb

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 を返すようにしてみました。


app/graphql/types/query_type.rb

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


BrandTypeShopType と同じ様に作成してみました。


zsh

$ rails g graphql:object Brand id:ID! name:String!


ちなみに、Rails のモデルやコントローラーと同じ様に rails d で消せます


zsh

$ rails d graphql:object Brand id:ID! name:String!



app/graphql/types/brand_type.rb

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 のときに、


app/models/shop.rb

class Shop < ApplicationRecord

belongs_to :brand, required: true
has_many :reviews, dependent: :destroy
~~
end


app/models/brand.rb

class Brand < ApplicationRecord

has_many :shops, dependent: :destroy
has_many :reviews, through: :shops, dependent: :destroy
~~
end

ShopTypefield の型を BrandType とすると、


app/graphql/types/shop_type.rb

module Types

class ShopType < Types::BaseObject
description '店舗'

~~
field :brand, BrandType, 'ブランド', null: false
end
end


次のように、呼び出せます。


1:N のアソシエーションの場合

Shop と Review が 1:N のとき、


app/models/shop.rb

class Shop < ApplicationRecord

~~
has_many :reviews, dependent: :destroy
~~
end


app/models/review.rb

class Review < ApplicationRecord

~~
belongs_to :shop
~~
end


zsh

$ be rails g graphql:object Review



app/graphql/types/review_type.rb

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


ShopTypefield の型を [ReviewType] とすると、


app/graphql/types/shop_type.rb

module Types

class ShopType < Types::BaseObject
description '店舗'

~~
field :reviews, [ReviewType], 'レビュー', null: false
end
end


次のように呼び出せます。


Shop を店舗名であいまい検索してみる


app/graphql/types/query_type.rb

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 モデルに定義されているインスタンスメソッドを呼んでみる


app/models/shop.rb

class Shop < ApplicationRecord

~~

# 全レビューの総合点平均
def overall_score
~~
end
end


methodfield に定義すれば Query で呼ぶことができます。


app/graphql/types/shop_type.rb

module Types

class ShopType < Types::BaseObject
description '店舗'

~~
field :overall_score, Float, '全レビューの総合点平均', null: true, method: :overall_score
end
end


スネークケースで定義した field は、GraphQL ではキャメルケースで呼ぶようになっていました。

overall_scoreoverallScore になります。


一度に複数の情報を取得

以下のように、同時に複数の情報を取得できます。


Mutation

前のバーションの情報が多くて、よくわからなかったので今後調べて書きます。


まとめ


  • 楽しい :dancer_tone1:

  • 結構簡単に実装できる :sunglasses:

  • 以前触ったときからの変更が多い :joy:


  • description を書いておくと勝手に Docs に追加される :thumbsup:


参考 :bow:


よくわかっていないところ :innocent:

わかり次第追記します。


  • Mutation



  • app/graphql/types/query_type.rb が増えてきたら読みづらそう


    • 分割すれば良い :question:




  • この辺


    • 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




最後に :santa:

PORT 株式会社では、自社サービスを支えてくれる優秀な Ruby エンジニアを募集しています(Ruby エンジニア以外も)。

もくもく会も行なっていますので、ぜひ一緒にもくもくしましょう!

PORT Advent Calendar はまだまだ続きます。乞うご期待!