21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PORTAdvent Calendar 2018

Day 12

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

Posted at

遅くなりましたが、 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 にアクセスしましょう。

アクセスすると、以下のような画面が表示されます。

image.png

この画面で実装後、すぐに動作確認できます。
また、右上の 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!" が返って来ました。

image.png

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

sample

さきほど作成した 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が返ってきます。

image.png

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

image.png

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

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

image.png

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

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

image.png

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

image.png

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

image.png

一度に複数の情報を取得

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

image.png

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 はまだまだ続きます。乞うご期待!

21
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?