Rails
ransack
Rails5

[Rails5+Ransack] 多対多のhas_many throughなアソシエーションで検索する

環境

$rails -v
Rails 5.1.6

$ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-darwin16]

$gem list
...
ransack (1.8.8)
...

多対多のアソシエーション(Ransack導入前)

has_many :hoges, through: hoge_fugasな、普通の多対多アソシエーション
必要ないメソッド定義などは省略。

# app/models/task.rb
class Task < ApplicationRecord
  has_many :task_categories, dependent: :destroy
  has_many :categories, through: :task_categories
  accepts_nested_attributes_for :categories
end
# app/models/category.rb
class Category < ApplicationRecord
  has_many :task_categories, dependent: :destroy
  has_many :tasks, through: :task_categories
end
# app/models/task_category.rb
class TaskCategory < ApplicationRecord
  belongs_to :task
  belongs_to :category
end

そして、最初コントローラは以下のような状態とします。

# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    @task = Task.new
    @categories = Category.all
  end
end

Ransack導入したい

まず、ビューをだいたい以下のようにします。
タスクに紐づいたカテゴリの名前をプルダウンで検索し、そのカテゴリ名に完全一致するカテゴリが紐づいたタスクを検索したい、という状況です。

# app/views/tasks/index.html.erb
<%= search_form_for @q do |f| %>
  <%= fields_for @categories do |cat| %>
    <%= f.collection_select(:SOME_SYMBOL_eq, @categories, :name, :name, include_blank: "未指定") %>
    <%= f.submit %>
  <% end %>
<% end %>

↑この状態まではググるとさくっと出てきます。
_eqは完全一致検索の時使います(equal)。部分一致の場合はシンボル末尾に_contをつけます(contain)。

ここからが問題。
アソシエーションがない場合や単純なhas_manyの場合はググればわかるのですが、上記SOME_SYMBOL_eqと書いた部分、どう設定すればよいかなかなかわかりません。

が、やっとここで見つけました。

Ransack導入

# app/views/tasks/index.html.erb
<%= search_form_for @q do |f| %>
  <%= fields_for @categories do |cat| %>
    <%= f.collection_select(:catagories_name_eq, @categories, :name, :name, include_blank: "未指定") %>
    <%= f.submit %>
  <% end %>
<% end %>

上記の通り、[紐づいたテーブル名]_[検索クエリとなるカラム名]_eqとすれば、やりたいことが実現できます。
:SOME_SYMBOL_eq:catagories_name_eqだったというわけです。

もちろんこれは、collection_selectではなくsearch_fieldなどでも使えます。

上の変更に伴って、コントローラにも修正を加えます。

# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    @task = Task.new
    @categories = Category.all
    @q = Task.ransack(params[:q]) # 追加
    @tasks = @q.result # 追加
  end

    # 以下追加
  def search 
    @q = Task.search(search_params)
    @tasks = @q.result 
  end
  private  
  def search_params
    params.require(:q).permit(:categories_name_eq)
  end
end

※検索フォームでsearch_fieldcollection_selectではなくcollection_check_boxesを使用する場合には、search_paramspermitのところを配列で許可する必要があると思います(やっていないのでわかりませんが)。