ransackなどのgemを使わずに、独自の検索ロジックを組んでいく(Ruby on Rails)

要件

Ruby on Railsのプロジェクトで検索機能といえば、gem ransackを使うことがあると思います。
gemは便利である一方、カスタマイズ性の調査に時間がかかったり、最悪ビジネス要件を満たせないケースもあります。
Railsでは絶対使うであろうgemがいくつかありますが、検索機能はロジックがわかれば自作した方が、自由に実装ができるため透明性の高いソースコードを作っていくことができます。
この記事ではそのTipsを記載したいと思います。

実装解説(Controller)

(1) supported_paramsには検索で使われるパラメータを制限しています

これらのパラメータを、引数も渡しながらメソッドチェーンしていきます。

(2) injectはブロックを使用して繰り返し処理を行います

第一引数のresultがinjectの初期値Userオブジェクトが入ります。第二引数のparamには、supported_paramsの要素が一つずつ入ります。

(3) パラメータの値があれば、レシーバresult(ここではUserオブジェクト)が持っているメソッドを、sendメソッドで呼び出します。

(4) パラメータの値がないか、レシーバがsupported_paramsで指定しているメソッドを持っていない場合は、レシーバ自身を返します。

supported_paramsの要素順に前回の戻り値とその要素の値がブロック引数に渡されて実行されます。

(5) 最後にpaginateのメソッドを別途チェーンします。


class ResumesController < ApplicationController
  def search
    @resumes =
      supported_params.inject(User) do |result, param|
        if params[param].present? and User.respond_to? "#{param}"
          result.send(:"#{param}", params[param])
        else
          result
        end
      end
      .page(params[:page])
  end

  private

  def supported_params
    [
      :ids_in,
      :age_gteq,
      :age_lteq,
      :keyword_eq,
    ]
  end
end

つまり、全ての検索項目に入力があった場合、こうなるわけです。
検索パラメータがない場合は、該当行のメソッドチェーンはされません。


  User.ids_in(params[:ids_in])
    .age_gteq(params[:age_gteq])
    .age_lteq(params[:age_lteq])
    .keyword_eq(params[:keyword_eq])
    .page(params[:page])

実装解説(Model)

続いて、Userモデルで、supported_paramsで指定しているメソッドをそれぞれ見ていきましょう。
賛否はありますが、ここではarelを使用しています。

class User < ApplicationRecord
  scope :ids_in, ->(ids) {
    where(User.arel_table[:id].in ids)
  }
  scope :age_gteq, ->(age) {
    joins(:profile)
      .where(Profile.arel_table[:age].gteq age)
  }
  scope :age_lteq, ->(age) {
    joins(:profile)
      .where(Profile.arel_table[:age].lteq age)
  }
  scope :keyword_eq, ->(keyword) {
    where(User.arel_table[:keyword].eq keyword)
  }
end

まとめ

gemは便利ですが、カスタマイズ性の調査に時間がかかったり、最悪ビジネス要件を満たせないケースもあったりするため、必要なものだけを使うようにし、必要ないものは自作で実装できると、全体の保守性も上がるかなと思っています。
要件や予算、開発期間に合わせて、自由に設計できるといいですね。