概要
- Userモデルのnameカラムで検索したい
- Userモデルのemailカラムで検索したい
- Bookモデルのtitleカラムで検索したい
といったように、同じモデルの異なるカラムや、そもそも異なるモデルのカラムに対して、名称検索をかけたくなることはよくあると思います。
そこで使える手法を整理します。
動作環境
Rails 4.2.11.1
ruby 2.4.5p335 (2018-10-18 revision 65137) [x86_64-darwin16]
愚直に実装する
必要な場所で、必要な数だけ scope
を生やします。
user.rb
class User < ActiveRecord::Base
scope :name_like, -> (value) {
pattern = ActiveRecord::Base.send(:sanitize_sql_like, value)
where('name LIKE ?', "%#{pattern}%")
}
scope :email_like, -> (value) {
pattern = ActiveRecord::Base.send(:sanitize_sql_like, value)
where('email LIKE ?', "%#{pattern}%")
}
end
book.rb
class Book < ActiveRecord::Base
scope :title_like, -> (value) {
pattern = ActiveRecord::Base.send(:sanitize_sql_like, value)
where('title LIKE ?', "%#{pattern}%")
end
module化する
例えば上の実装では value
が nil や空文字の場合に検索結果がなくなってしまいますが、
そのような場合には何もフィルタリングしない、という仕様変更があったとします。
愚直な実装では、あいまい検索を行いたいモデルの全てで検索ロジックを実装していますから、
検索方法に変更が必要になってしまったことを想像すると・・・気が引けますね。
そこで、検索ロジック部分を module
として切り離します。
fuzzy_match_searchable.rb
module FuzzyMatchSearchable
extend ActiveSupport::Concern
class_methods do
def fuzzy_match(column, value)
return current_scope if value.blank?
pattern = ActiveRecord::Base.send(:sanitize_sql_like, value)
where(%("#{table_name}"."#{column}" LIKE ?), "%#{pattern}%")
end
end
end
各モデルでは、以下のように実装します。
user.rb
class User < ActiveRecord::Base
include FuzzyMatchSearchable
scope :name_like, (value) -> { fuzzy_match('name', value) }
scope :email_like, (value) -> { fuzzy_match('email', value) }
end
book.rb
class Book < ActiveRecord::Base
include FuzzyMatchSearchable
scope :title_like, (value) -> { fuzzy_match('title', value) }
end
コードクローンを回避でき、とってもいい感じですね。
参考記事
[Rails] ActiveSupport::Concern の存在理由:https://qiita.com/castaneai/items/6dc121ce6ff100614f42
sanitize_sql_like
について:https://apidock.com/rails/v4.2.1/ActiveRecord/Sanitization/ClassMethods/sanitize_sql_like