railsで検索するならRansack!!!!!!!!!!!!

ransack を使ったときになぜか検索できなかった話

最近の悩みだったんですよ ()

Railsの検索フォームを作る時ってさりげなくめんどくさくないですか。

めんどくさいですよね;;... めんd .... ゲホゲホ (異論は認める)

+ = form_tag xxx_yyyy_search_path do
+   .row
+     .form-group.col-sm-2
+       = text_field_tag :xxx_name,
+       { placeholder: "xxx", class: "form-control input-sm" }
+     .form-group.col-sm-2
+       = text_field_tag :yyy_name,
+       { placeholder: "yyy", class: "form-control input-sm" }
+      .form-group.col-sm-2
+        = submit_tag '検索するよー', class: 'btn btn-default btn-sm'
+def search 
+  @samples = Sample.page(params[:page]).per(PER)
+  if params[:xxx_name].present?
+    @sample = @sample.joins(zzz: :xxx)
+                     .where('xxx.name like ?', "%#{params[:xxx_name]}%")
+  end
+
+  if params[:yyy_name].present?
+    @sample = @sample.where('name like ?', "%#{params[:name]}%") 
+  end
+  
+  @sample = @sample.decorate
+end

これ。死ぬほど冗長だし、ちょっと(どころじゃなく)見栄えが悪いんですよね。 検索したいcolumnがふえるたびにViewに

+ .form-group.col-sm-2
+   = text_field_tag :new_column,
+   { placeholder: "検索したいcolumnふえたよ!", class: "form-control input-sm" }

↑のようなViewを追加し、

+  if params[:new_column].present?
+    @sample = @sample.where('new_column like ?', "%#{params[:new_column]}%") 
+  end

のように追加しないといけないんですよ。 DBがふくらんで3個どころじゃなく検索したくなったらどうするつもりですか?????? (心の声:頑張る) ←やめてください

そんな時に使うのが Ransack というGemです (https://github.com/activerecord-hackery/ransack

これがとても素晴らしいもので、早い、すごい、便利の三拍子を揃えているのです!

と、いうわけで

ransackの紹介と導入について

簡単に...(きになる方は本家みてw)

ransackは...README.

Ransack enables the creation of both simple and advanced search forms for your Ruby on Rails application (demo source code here). 
If you're looking for something that simplifies query generation at the model or controller layer,
you're probably not looking for Ransack (or MetaSearch, for that matter). Try Squeel instead.

だそうです。まあつまり シンプルだけど高度な検索をしたいならコレ!というわけです。 (雑)

ransackを使うためには

gem 'ransack'

もしくは

gem 'ransack', github: 'activerecord-hackery/ransack' # if you would like to use lates updates, use the master branch

をGemfileに記載し $ bundleするだけでじゅんびは終わりです。

def index
  @q = Person.ransack(params[:q])
  @people = @q.result(distinct: true)
end
<%= search_form_for @q do |f| %>

  # Search if the name field contains...
  <%= f.label :name_cont %>
  <%= f.search_field :name_cont %>

  # Search if an associated articles.title starts with...
  <%= f.label :articles_title_start %>
  <%= f.search_field :articles_title_start %>

  # Attributes may be chained. Search multiple attributes for one value...
  <%= f.label :name_or_description_or_email_or_articles_title_cont %>
  <%= f.search_field :name_or_description_or_email_or_articles_title_cont %>

  <%= f.submit %>
<% end %>

とすれば良いみたいです。個人プロジェクトに入れたのだいぶ昔でサンプルないので公式をコピペ。

search_form_for

というのがRansackが提供しているメソッドでRansackを使う検索をする場合はForm_forではなくこれを利用します。
  上記のような記載にしてあげることでRansack様の恩恵を受けられます。   またransackを利用するさいにはsearch_filedへ引数を利用することで検索条件を変更することが可能です。

上記のような、

<%= f.search_field :name_cont %>
<%= f.search_field :articles_title_start %>

という場合は name columnに対してLike検索、 関連先の、articlesのtitleに対してのlike検索。を行います。 (多分 has_many articlesの関係)

  その他にも、https://github.com/activerecord-hackery/ransack

シンボル引数 内部条件
column_eq 完全一致
column_matches like検索
column_cont 部分一致
column_true bool検索
column_blank blankかどうか
column_present 存在確認
column_null null確認
not column_not_eq みたいに否定ができる

などなどあります。 自分は公式のドキュメントや、

http://nekorails.hatenablog.com/entry/2017/05/31/173925

↑のサイトをみたりしてます。


そんな便利なRansackさんですが、

セキュリティ的なことを考えると機能によってはチューニングしないといけないのです。

今回自分はそれに気づくまで時間をとかしてしまったので、猛省しながら本記事を書こうと思ったわけです。

ちなみに今回僕が遭遇したのは、

特定のcolumnでは検索できるのに 他のcolumnでは検索ができない。

まずは遭遇したエラー文ですが↓

 undefined method `xxx_cont' for Ransack::Search<class: Entry, base: Grouping <combinator: and>>:Ransack::Search

うーん、エラー文としてはよくあるアレですよね、
Nomethod error ........ ウルセェ!モデルに columnあるだろ!!!! f●ck!!!!!!!!!!

ただ、この時もControllerにて

def action
  @q = Model.search(params[:q])
  @instance_valiables = @q.result
end

のようにしてたわけで、この @q の内容は

=> Ransack::Search<class: Model, base: Grouping <combinator: and>>

になっており、正常にRansackのメソッドになってるんですよね。うーん。

RailsContsoleで

Model.ransack({xxx_cont: 'sample'}).result

→と、すれば該当モデルの'xxx'カラムに対して検索もしてくれてる風なレスポンスですしお寿司。。。 (今思えば、実際は検索されていないで Model.allの結果になってたっぽい気がする)

↑↑↑↑↑↑

とまぁ。状況の創造はできたと思うので実際の対処になります。

該当columnで検索してないのに絞り込めてない時の対処が以下になります。

問題としては該当モデルのファイルにて

self.ransackable_attributes self.ransackable_associations

ransackのオーヴァーライドが原因になっていました。 自分が作業をしていた時は

  def self.ransackable_attributes(*)
    %w[created_at]
  end

  def self.ransackable_associations(*)
    %w[]
  end

となっており、検索したい name columnが検索対象の columnとして登録がされておらず。 また、関連先の定義がされていなかったため、articleに対する検索すらもできなかったということになります。。。。。。。。。。。。。。。。。。

つらい。 しんどぃ。。

なので、以下のメソッドに変更を加え、

  def self.ransackable_attributes(*)
    %w[created_at name]
  end

  def self.ransackable_associations(*)
    %w[articles]
  end

のようにすると name columnでも検索が可能になりますし、articleへの探索も可能になるのです。

これで動くのでひとあんしんですね。

余談

まぁなんというか、エラーでて調べてもこの手のエラーへの対処がなかったので今回書いてみたんですが。 行ってしまえば、自明な内容になってしまいそうなので、今回はさらに一歩踏み込んだ内容について考えてみようかなと。

ransackのsource codeをのぞいてみよう。。。!

初めてOSS?をコード見るためにCloneしました。
はるか昔にDevise(認証用のGem)をみたことがあるが黒魔術すぎて3秒で諦めたなぁと物思いに老けながらgit clonne...)

今回の諸悪の根源である該当メソッドは以下のファイルにて定義されています。 (https://github.com/activerecord-hackery/ransack/blob/master/lib/ransack/adapters/active_record/base.rb)

 # Ransackable_attributes, by default, returns all column names
 # and any defined ransackers as an array of strings.
 # For overriding with a whitelist array of strings.
 #
 def ransackable_attributes(auth_object = nil)
    @ransackable_attributes ||= if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
      column_names + _ransackers.keys + _ransack_aliases.keys +
      attribute_aliases.keys
    else
      column_names + _ransackers.keys + _ransack_aliases.keys
    end
 end

 # Ransackable_associations, by default, returns the names
 # of all associations as an array of strings.
 # For overriding with a whitelist array of strings.
 #
 def ransackable_associations(auth_object = nil)
   @ransackable_associations ||= reflect_on_all_associations.map { |a| a.name.to_s }
 end

がっつりコメントアウトされとるやんけ!!!!!!!!

# Ransackable_attributes, by default, returns all column names # and any defined ransackers as an array of strings. # For overriding with a whitelist array of strings. # ちょっとした時に使う分には全ての columnにたいしての検索でもいい。ということからデフォルトは全ての columnをreutrnしている。

しかしながら、悪意のあるユーザにとってはこれがSQLインジェクションの入り口になったり? インジェクションはできなくても製作者が意図してない検索をさせることも容易に想像ができます。

そのため今回のプロジェクトでは該当メソッドをオーバーライドして検索をかける場合は自分が許可したものしか通さないんですね。 うーんStrongParameterに似ている気がするw

ちなみに改めてREADMEをみていると

Authorization (whitelisting/blacklisting)

ちゃんと記載してありました。Document読みましょう。

まとめ。

documentを読みましょう。答えはそこにある。。