11
11

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.

GraphQLAdvent Calendar 2019

Day 12

graphql-rubyでransackの検索を利用する

Last updated at Posted at 2019-06-13

TL;DR

検索ロジック

module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      description "Find a houses ransack"
      argument :q, BaseScalar, required: false # これが`any`タイプ
      argument :page, Integer, required: false
      argument :per, Integer, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      if q.present?
        @q = House.joins(:city).ransack(q)
        @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
        @q.result(distinct: true).page(page).per(per)
      else
        House.all
      end
    end
  end
end

GraphQL

{
   houses(q: {city_name_cont: "London", s: "name desc"}, page: 2, per: 4) {
    name
  }
}

結果

{
  "data": {
    "houses": [
      {
        "name": "Thomas"
      },
      {
        "name": "Ruby"
      },
      {
        "name": "Kelly"
      },
      {
        "name": "Jeffrey"
      }
    ]
  }
}

解説

QueryType

まず、GraphQLはSchemaの定義が要求されているが、Ransackの述語という概念と相性があまり良いとは言えません。
そこで出された案はanyタイプを利用することでした。GraphQLではBaseScalarというタイプはanyタイプと近い使い方ができます。
下記のコードのように、qというパラメータに任意のqueryを送信できます。

query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
    end
    def houses(q: nil, page: nil, per: nil)
    end
  end
end
{
   houses(q: {city_name_cont: "London"}) {
    name
  }
}

Ransackの検索

Ransackについて詳しいの解説はこちらでは割愛いたします。こちらの記事を参考して、qパラメータを組み立て、House.joins(:city).ransack(q)のようにQueryを投げ込めばいいです。良いのも悪いのも、述語が間違っても、Ransackはエラーが発生しないことでした。

検索結果の並び替え

q: { s: 'id DESC' }のように、qパラメータの中に、sフィールドを利用すれば、簡単に並び替えられます。また、q: { s: ['id DESC', 'name asc'] }のように、配列を与えば、複数条件での並び替えも可能です。
デフォルトの並び替えについてですが、こちらの記事を参考して、以下のロジックを実装すれば簡単にできます。

query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      @q = House.joins(:city).ransack(q)
      @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
      @q.result(distinct: true)
    end
  end
end

Kaminariでページネーション

普通、フォームで検索をする際、Kaminariparams[:page]を利用してページ切り替えを行います。ここでは似たような書き方で、pageQueryを定義します。また、フォームで検索をする場合、windowのサイズを固定するのが一般ですが、APIで検索する際、windowのサイズを可変にする方が使い勝手がいいので、perQueryも追加します。

query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
      argument :page, Integer, required: false
      argument :per, Integer, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      @q = House.joins(:city).ransack(q)
      @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
      @q.result(distinct: true).page(page).per(per)
    end
  end
end

おまけ

ユーザー認証機能

デフォルトの状態では、GraphQLは誰でも検索可能な状態で非常によくありません、JWTなどでログイン機能を追加することができます。

app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  skip_before_action :verify_authenticity_token # GraphQLはPOSTなので、ないと怒られる
  before_action :authenticate_user!, unless: :from_graphiql?

  # 省略...

  private

  # 開発環境でgraphiqlが開けなくなる恐れがあるので、パイパスを作る
  def from_graphiql?
    referrer = request.referer
    if Rails.env.development? && referrer =~ /\/graphiql/
      return true
    end
  end
end

また、executeActionにcontextを設置すれば、query_type.rbではcurrent_userなどが使用可能になります。

app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  # 省略...

  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      # Query context goes here, for example:
      current_user: current_user,
    }
    result = CaesarisSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  rescue => e
    raise e unless Rails.env.development?
    handle_error_in_development e
  end

  # 省略...
end

camelCaseでの検索

GraphQLの作法はcamelCaseですが、RansackなどRailsのGemはsnake_caseであるため、houses(q: {city_name_cont: "London"})のようなQueryは非常に気分が悪いです。

下記のメソッドを実装すれば、camelCaseでの検索も可能になります。

query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
      argument :page, Integer, required: false
      argument :per, Integer, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      @q = House.joins(:city).ransack(form_ransack_params(q))
      @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
      @q.result(distinct: true).page(page).per(per)
    end
  end

  def form_ransack_params(params)
    param_res = {}
    params.each do |key, val|
      key = key.to_s.underscore
      if (key == 's' || key == 'sorts') && val.instance_of?(String)
        param_res[key] = val.split(/\s+/).map(&:underscore).join(' ').strip
      elsif (key == 's' || key == 'sorts') && val.instance_of?(Array)
        param_res[key] = val.map{|v| v.split(/\s+/).map(&:underscore).join(' ').strip }
      elsif (key == 'g' || key == 'groupings') && (val.instance_of?(Hash) || val.instance_of?(Array))
        if val.instance_of?(Hash)
          param_res[key] = val.values.map{|v| Util.form_ransack_params(v) }
        else
          param_res[key] = val.map{|v| Util.form_ransack_params(v) }
        end
      else
        param_res[key] = val
      end
    end
    param_res
  end
end
{
   houses(q: {cityNameCont: "London", s: "cityName DESC"}) {
    name
  }
}

参考

https://stackoverflow.com/questions/45598812/graphql-blackbox-any-type
https://qiita.com/jerrywdlee/items/a08589ea160c470e67f6
https://qiita.com/toshiotm@github/items/c7909b9bb97f845d831a
https://github.com/activerecord-hackery/ransack
https://github.com/rmosolgo/graphql-ruby/blob/master/guides/type_definitions/lists.md
https://github.com/rmosolgo/graphql-ruby/blob/master/guides/fields/arguments.md

11
11
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
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?