WeBlog

Webに関する情報を中心に発信してるブログ

ransackで検索機能を実現する方法

ransackとは?

ransackはRailsで検索機能や表示順番を並び替える(ソート)機能を実現するためのgemです。

ページネーションを使って商品が1つの画面に30件とか表示されると、その中からお目当ての商品を探すのはとても大変です。

そんな時に「特定のキーワード」や「金額が安い/高い順番」「カテゴリー」とかで検索したり、並び替える(ソート)ことができると便利です。

そういった検索機能を簡単に実現できるのがransackというgemです。

https://github.com/activerecord-hackery/ransack#simple-mode

ransackの使い方

ransackを使うためにはGemfileにransackを書いてbundle installします。

gem 'ransack'

これでransackを使えるようになります。

コントローラーを編集する

class ProductsController < ApplicationController

  # 商品一覧を取得する(検索して絞り込めるようにした)
  def index
    @q = Product.ransack(params[:q])
    @products = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end
    
    # いいねした商品を取得する(検索して絞り込めるようにした)
    def likes
    @q = current_user.like_produsts.ransack(params[:q])
    @like_produsts = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end

  # 商品詳細画面(商品に対してコメントした場合に、そのコメントを絞り込む)
    def show
    @product = Product.find(params[:id])
        # 1つの商品にされたコメントで検索(絞り込み)したいので、1つの商品のコメントに対して.ransack(params[:q])を書く
    @q = @product.comments.ransack(params[:q])
    @comments = @q.result(distinct: true).includes(:user).order(created_at: :desc)
  end

上記は商品を一覧で取得するコードです。

.includes(:user)より後ろの部分はransackとは関係ないです。

.includes(:user)・・・「N + 1」問題を解決するためコード

.order(created_at: :desc)・・・DESC順に並び替えるコード

.page(params[:page])・・・1つの画面に表示する商品などの数

ここで注目するのはransack()メソッドやresult(distinct: true)というメソッドです。

params[:q]はサーバー側から送られてくる検索したい文字を取得するために書いています。

ransack()メソッドは「送られてきたパラメーターを元にテーブルからデータを検索する」メソッドです。

なんで今回で言うと、productsテーブルからparams[:q]を検索しています。

result(distinct: true)は、ransackのメソッドで「取得したデータをActiveRecord_Relationのオブジェクトに変換する」メソッドです。

ビュー側の書き方

<%= search_form_for @q, url: boards_path do |f| %>
     <%= f.search_field :title_cont, class: 'form-control', placeholder: '検索ワード' %>
   <%= f.submit t('defaults.search'), class: 'btn btn-primary' %>
<% end %>

##########################
# HTMLは下記のようになる
##########################

# HTML(検索していない時)
<form class="board_search" id="board_search" action="/boards" accept-charset="UTF-8" method="get">
<input name="utf8" type="hidden" value="">
     <input class="form-control" placeholder="検索ワード" type="search" name="q[title_cont]" id="q_title_cont">
     <input type="submit" name="commit" value="検索" class="btn btn-primary" data-disable-with="検索">
</form>

# HTML(Donで検索した時)
<form class="board_search" id="board_search" action="/boards" accept-charset="UTF-8" method="get">
<input name="utf8" type="hidden" value="">
      <input class="form-control" placeholder="検索ワード" type="search" value="Don" name="q[title_cont]" id="q_title_cont">
      <input type="submit" name="commit" value="検索" class="btn btn-primary" data-disable-with="検索" disabled="">
</form>

search_form_forが検索のためのformを作成してくれます。

なのでHTMLをみてみるとtype="search"となっています。

search_form_forの引数には@qを渡してあげます。

@qの中には検索した文字列などが入っているので、例えば「Don」と言う文字列で検索した場合にHTMLをみてみるとvalue="Don"と言う値がデフォルトで設定されます。

これは@qを引数で渡しているからです。

当然検索していない時は@qの中には何もないので、value属性は設定されていません。

url:にこの検索フォームが通信する先のURLを指定します。

そうするとHTMLをみてみるとaction="/boards"と言う形で設定されます。

HTTPのメソッドはGETで設定されます。

f.search_field :title_contと書いてありますが、:title_contの部分は大切です。

titleが検索をしたいテーブルのカラム名です。

なので、ここではtitleカラムを検索の対象にしています。

_contが検索の条件です。

_contは部分一致検索をするための条件なので、SQLを見るとLIKE '%Don%'みたいなSQLが走ります。

HTMLをみてみるとname="q[title_cont]"となっています。

こいつがキーとなってフォームに入力した値が紐づいてサーバー側に投げられます。

それをコントローラー側で.ransack(params[:q])で取得して検索しています。

他にもいろんな検索の条件があります。

参考資料:https://github.com/activerecord-hackery/ransack/wiki/Basic-Searching

ビュー側で複数のカラムを検索対象にする

ちなみ1つのテーブルの複数のカラムを検索対象としたい場合は下記のように書きます。

<%= search_form_for q, url: boards_path do |f| %>
  <%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: t('defaults.search_word') %>
  <%= f.submit class: 'btn btn-primary' %>
<% end %>

##########################
# HTMLは下記のようになる
##########################

<form class="board_search" id="board_search" action="/boards" accept-charset="UTF-8" method="get"><input name="utf8" type="hidden" value="">
  <input class="form-control" placeholder="検索ワード" type="search" name="q[title_or_body_cont]" id="q_title_or_body_cont">
  <input type="submit" name="commit" value="検索" class="btn btn-primary" data-disable-with="検索">
</form>

ここで注目するべきは:title_or_body_contという部分です。

カラム名_or_カラム名_検索条件と書くと、複数のカラムを検索の対象にします。

この例で言うとtitleカラムまたはbodyカラムに検索したい文字列があるものを取得します。

HTMLを見てみると、name="q[title_or_body_cont]"と言う形で先ほどと変わっています。

params[:q]、.ransack()、.result()、distinct: true って何?

ちょっとここでparams[:q]、.ransack()、result()、distinct: trueが何をしているのかコンソールで見てみます。

(distinct: true)なしで、何も検索していない場合(普通に画面を開いた)

class BoardsController < ApplicationController

  # GET /boards
  # GET /boards.json
  def index
    @q = Board.ransack(params[:q])
    @boards = @q.result.includes(:user).order(created_at: :desc).page(params[:page])
  end

####################################
# rails console(最初に画面を開いた時)
####################################

> params
=> <ActionController::Parameters {"controller"=>"boards", "action"=>"index"} permitted: false>

> params[:q]
=> nil

> @q
=> Ransack::Search<class: Board, base: Grouping <combinator: and>>

> @q.result
  Board Load (0.4ms)  SELECT "boards".* FROM "boards"
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabc569d3b8
  id: 1,
  title: "タイトル1",
  body: "本文1",
  user_id: 1,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabc569d228
  id: 2,
  title: "タイトル2",
  body: "本文2",
  user_id: 2,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,
以下続く

> @q.result.includes(:user)
  CACHE Board Load (1.0ms)  SELECT "boards".* FROM "boards"
  ↳ app/controllers/boards_controller.rb:11
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 2], ["id", 3], ["id", 4]]
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabc84c4930
  id: 1,
  title: "タイトル1",
  body: "本文1",
  user_id: 1,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabc84c47f0
  id: 2,
  title: "タイトル2",
  body: "本文2",
  user_id: 2,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,

> @q.result.includes(:user).order(created_at: :desc)
  Board Load (1.4ms)  SELECT "boards".* FROM "boards" ORDER BY "boards"."created_at" DESC
  ↳ app/controllers/boards_controller.rb:11
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 4], ["id", 2], ["id", 3]]
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabaa0c7d78
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabaa0c7b48
  id: 105,
  title: "Baby Five",
  body: "Mogu Mogu no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,

> @q.result.includes(:user).order(created_at: :desc).page(params[:page])
  Board Load (0.4ms)  SELECT  "boards".* FROM "boards" ORDER BY "boards"."created_at" DESC LIMIT ? OFFSET ?  [["LIMIT", 20], ["OFFSET", 0]]
  ↳ app/controllers/boards_controller.rb:11
  CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 4], ["id", 2], ["id", 3]]
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabc887c7f8
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabc887c6b8
  id: 105,
  title: "Baby Five",
  body: "Mogu Mogu no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,

(distinct: true)なしで、何も検索していない場合(普通に画面を開いた)は上記のような感じになります。

検索していないのでparams[:q]nilです。

@qの中はRansack::Search<class: Board, base: Grouping <combinator: and>>と言う風になっています。

@q.resultSQLが走っていることがわかります。

resultSQLを走らせて検索してくれて、ransack()SQLが走る前の検索の準備と言う感じです。

params[:q]nilの時は、SQLを見るとwhereで条件を絞ることなく検索しています。

(distinct: true)なしで、「Don」と言う文字で検索した場合

class BoardsController < ApplicationController
  before_action :set_board, only: %i[edit update destroy]

  # GET /boards
  # GET /boards.json
  def index
    @q = Board.ransack(params[:q])
    @boards = @q.result.includes(:user).order(created_at: :desc).page(params[:page])
  end

####################################
# rails console(検索した時)
####################################

> params
=> <ActionController::Parameters {"utf8"=>"", "q"=><ActionController::Parameters {"title_cont"=>"Don"} permitted: false>, "commit"=>"検索", "controller"=>"boards", "action"=>"index"} permitted: false>

> params[:q]
=> <ActionController::Parameters {"title_cont"=>"Don"} permitted: false>

> @q
=> Ransack::Search<class: Board, base: Grouping <conditions: [Condition <attributes: ["title"], predicate: cont, values: ["Don"]>], combinator: and>>

> @q.result
  Board Load (1.2ms)  SELECT "boards".* FROM "boards" WHERE "boards"."title" LIKE '%Don%'
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabc803f270
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabc803f0b8
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

> @q.result.includes(:user)
  CACHE Board Load (0.0ms)  SELECT "boards".* FROM "boards" WHERE "boards"."title" LIKE '%Don%'
  ↳ app/controllers/boards_controller.rb:11
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 2], ["id", 1]]
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabc51a7480
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabc51a5ec8
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

> @q.result.includes(:user).order(created_at: :desc)
  Board Load (0.4ms)  SELECT "boards".* FROM "boards" WHERE "boards"."title" LIKE '%Don%' ORDER BY "boards"."created_at" DESC
  ↳ app/controllers/boards_controller.rb:11
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 1], ["id", 2]]
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabc905e160
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabc905dff8
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

> @q.result.includes(:user).order(created_at: :desc).page(params[:page])
  Board Load (0.2ms)  SELECT  "boards".* FROM "boards" WHERE "boards"."title" LIKE '%Don%' ORDER BY "boards"."created_at" DESC LIMIT ? OFFSET ?  [["LIMIT", 20], ["OFFSET", 0]]
  ↳ app/controllers/boards_controller.rb:11
  CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 1], ["id", 2]]
  ↳ app/controllers/boards_controller.rb:11
=> [#<Board:0x00007fabc846c2f8
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007fabc846c190
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

検索するとparams[:q]に値が入っています。

@qに関しても、<conditions: [Condition <attributes: ["title"], predicate: cont, values: ["Don"]>]と言う部分が増えています。

attributes: ["title"]が対象のカラム、predicate: cont,が検索条件、values: ["Don"]が検索したい文字列です。

これを元にしてSQLを走らせて検索をしています。

@q.resultSQLが走っています。

その際のSQLにはLIKE '%Don%'という部分一致検索をしていることがわかります。

なのでcontと言う検索の条件がLIKE '%Don%'と言う形の検索をしてくれています。

また、params[:q]に値が入っている場合は、SQLWHEREを使って絞り込んでいます。

(distinct: true)ありで、何も検索していない場合

class BoardsController < ApplicationController
  before_action :set_board, only: %i[edit update destroy]

  # GET /boards
  # GET /boards.json
  def index
    @q = Board.ransack(params[:q])
    @boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end

> params
=> <ActionController::Parameters {"controller"=>"boards", "action"=>"index"} permitted: false>

> params[:q]
=> nil

> @q
=> Ransack::Search<class: Board, base: Grouping <combinator: and>>

> @q.result(distinct: true)
  Board Load (7.6ms)  SELECT DISTINCT "boards".* FROM "boards"
  ↳ app/controllers/boards_controller.rb:13
=> [#<Board:0x00007f8872310b30
  id: 1,
  title: "タイトル1",
  body: "本文1",
  user_id: 1,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f88723107e8
  id: 2,
  title: "タイトル2",
  body: "本文2",
  user_id: 2,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,

以下続く

> @q.result(distinct: true).includes(:user)
  CACHE Board Load (0.0ms)  SELECT DISTINCT "boards".* FROM "boards"
  ↳ app/controllers/boards_controller.rb:13
  User Load (5.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 2], ["id", 3], ["id", 4]]
  ↳ app/controllers/boards_controller.rb:13
=> [#<Board:0x00007f8871da80f8
  id: 1,
  title: "タイトル1",
  body: "本文1",
  user_id: 1,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f8871db3c28
  id: 2,
  title: "タイトル2",
  body: "本文2",
  user_id: 2,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  board_image: nil>,

> @q.result(distinct: true).includes(:user).order(created_at: :desc)
  Board Load (1.6ms)  SELECT DISTINCT "boards".* FROM "boards" ORDER BY "boards"."created_at" DESC
  ↳ app/controllers/boards_controller.rb:13
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 4], ["id", 2], ["id", 3]]
  ↳ app/controllers/boards_controller.rb:13
=> [#<Board:0x00007f8872729f78
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f8872729ac8
  id: 105,
  title: "Baby Five",
  body: "Mogu Mogu no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,

> @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  Board Load (0.6ms)  SELECT  DISTINCT "boards".* FROM "boards" ORDER BY "boards"."created_at" DESC LIMIT ? OFFSET ?  [["LIMIT", 20], ["OFFSET", 0]]
  ↳ app/controllers/boards_controller.rb:13
  CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 4], ["id", 2], ["id", 3]]
  ↳ app/controllers/boards_controller.rb:13
=> [#<Board:0x00007f8873f49478
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f8873f49338
  id: 105,
  title: "Baby Five",
  body: "Mogu Mogu no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,

(distinct: true)ありで、「Don」と言う文字で検索した場合

class BoardsController < ApplicationController
  before_action :set_board, only: %i[edit update destroy]

  # GET /boards
  # GET /boards.json
  def index
    @q = Board.ransack(params[:q])
    @boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end

> params
=> <ActionController::Parameters {"utf8"=>"", "q"=><ActionController::Parameters {"title_or_body_cont"=>"Don"} permitted: false>, "commit"=>"検索", "controller"=>"boards", "action"=>"index"} permitted: false>

> params[:q]
=> <ActionController::Parameters {"title_or_body_cont"=>"Don"} permitted: false>

> @q
=> Ransack::Search<class: Board, base: Grouping <conditions: [Condition <attributes: ["title", "body"], predicate: cont, combinator: or, values: ["Don"]>], combinator: and>>

> @q.result(distinct: true)
  Board Load (0.4ms)  SELECT DISTINCT "boards".* FROM "boards" WHERE ("boards"."title" LIKE '%Don%' OR "boards"."body" LIKE '%Don%')
  ↳ app/controllers/boards_controller.rb:10
=> [#<Board:0x00007f8874612750
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f8874612610
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

> @q.result(distinct: true).includes(:user)
  CACHE Board Load (0.0ms)  SELECT DISTINCT "boards".* FROM "boards" WHERE ("boards"."title" LIKE '%Don%' OR "boards"."body" LIKE '%Don%')
  ↳ app/controllers/boards_controller.rb:10
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 2], ["id", 1]]
  ↳ app/controllers/boards_controller.rb:10
=> [#<Board:0x00007f8875bb49a0
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f8875bb4838
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

> @q.result(distinct: true).includes(:user).order(created_at: :desc)
  Board Load (0.4ms)  SELECT DISTINCT "boards".* FROM "boards" WHERE ("boards"."title" LIKE '%Don%' OR "boards"."body" LIKE '%Don%') ORDER BY "boards"."created_at" DESC
  ↳ app/controllers/boards_controller.rb:10
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 1], ["id", 2]]
  ↳ app/controllers/boards_controller.rb:10
=> [#<Board:0x00007f887474b860
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f887474b720
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

> @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  Board Load (1.0ms)  SELECT  DISTINCT "boards".* FROM "boards" WHERE ("boards"."title" LIKE '%Don%' OR "boards"."body" LIKE '%Don%') ORDER BY "boards"."created_at" DESC LIMIT ? OFFSET ?  [["LIMIT", 20], ["OFFSET", 0]]
  ↳ app/controllers/boards_controller.rb:10
  CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 1], ["id", 2]]
  ↳ app/controllers/boards_controller.rb:10
=> [#<Board:0x00007f8873d414a0
  id: 106,
  title: "Don Chinjao",
  body: "Hebi Hebi no Mi",
  user_id: 1,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>,
 #<Board:0x00007f8873d41360
  id: 94,
  title: "Donquixote Doflamingo",
  body: "Doku Doku no Mi",
  user_id: 2,
  created_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  updated_at: Tue, 13 Apr 2021 14:24:12 JST +09:00,
  board_image: nil>]

コントローラー側で@q.result(distinct: true)と言うようにdistinct: trueと言うのをつけると、SQL文にSELECT DISTINCT "boards".* FROMと言う形でDISTINCTがついています。

このDISTINCTってなんでしょうか?

ransackにおけるdistinct: trueの必要性について

distinct: trueが必要になるのは、「関連する子テーブルの情報を条件に絞り込んで、親テーブルの検索結果を表示するとき」のケースです。

f:id:weblog_tec:20210503094814p:plain

例えば、上記の画像のようにcommentsテーブルがあって、そこに2つのコメントのレコードがあります。

このcommentsテーブルには、board_idと言うカラムがあります。

要は、上記の画像で言うと、boardsテーブルのidカラムが「26番」の掲示板に対してコメントをしたと言うことです。

なので、boardsテーブルが親にあたり、commentsテーブルが子にあたります。

なので、子であるcommentsテーブルの条件を絞り込んで、絞り込んだcommentsテーブルに紐ずくboardsテーブルの情報を取得するパターンにおいて、distinct: trueが必要になります。

実際にSQLを走らせてみます。

f:id:weblog_tec:20210503094856p:plain

上記の画像をみてもらうと、SQL文はboardsテーブルにcommentsテーブルを内部結合(INNER JOIN)して、LIKEを使って、commentsテーブルのbodyカラムに「ユーザーA」という文字が入っているやつを絞り込んでいます。

そうすると、「ユーザーA」という文字が入っているレコードが2つあるので、boardsテーブルも2つレコードが取れます。

しかし、boardsテーブルのidカラムをみてみると、同じidです。

要は同じ掲示板を2つ取得しています。

この掲示板を画面に表示すると、同じ掲示板が2つ表示される画面になります。

f:id:weblog_tec:20210503094938p:plain

上の画像ではboardsテーブルとcommentsテーブルを結合して、boardsテーブル側もcommentsテーブル側も両方取得していますが、boardsテーブル側だけを取得すると、上記のようになります。

f:id:weblog_tec:20210503094956p:plain

distinct: trueをつけるとどうなるかを確認してみます。

distinct: trueをつけないと掲示板が2件取得されましたが、distinct: trueをつけると上記の画像のように1件だけ取得できました。

同じレコードが結果に複数件表示される場合に、distinctを使って重複を取り除くことができます。

なので、このような場合にdistinct: trueが必要になります。

なので、「現在ログインしているユーザーがお気に入りした商品の中で、「カメラ」という文字が入っている商品だけ取得したい」という場合は、

current_user.bookmark_products.ransack(params[:q])

のようになるので、この場合は

usersテーブル→bookmarksテーブル→productsテーブル

という順番で辿っていって、そこでproductsテーブルのあるカラムにparams[:q]が入っているやつを検索するので、絞り込んでいるのはあくまでproductsテーブルになります。

こういう場合は必要ないです。

distinct: trueをつける必要があるのか?必要ないのか?考えるの面倒ですが、検索時に重複したデータを結果として表示したいケースはほとんどないですので、distinct: trueは毎回つけておくと良いです。

search_form_forを使用する際、urlオプションを指定しないとどうなるか?

class BoardsController < ApplicationController

  # 掲示板一覧
  def index
    @q = Board.ransack(params[:q])
    @boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end

    # ユーザーがブックマークした掲示板を取得
    def bookmarks
    @q = current_user.bookmark_boards.ransack(params[:q])
    @bookmark_boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end
<%= search_form_for @q do |f| %>
  <%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: t('defaults.search_word') %>
    <%= f.submit class: 'btn btn-primary' %>
<% end %>

#############
# HTML
#############

<form class="board_search" id="board_search" action="/boards" accept-charset="UTF-8" method="get"><input name="utf8" type="hidden" value="">
<div class="input-group mb-3">
  <input class="form-control" placeholder="検索ワード" type="search" name="q[title_or_body_cont]" id="q_title_or_body_cont">
  <div class="input-group-append">
    <input type="submit" name="commit" value="検索" class="btn btn-primary" data-disable-with="検索">
  </div>
</div>
</form>

search_form_forの引数のurl: boards_pathを削除しています。

この状態でHTMLを見るとaction="/boards"と言う設定になっています。

この状態で、ブックマーク一覧画面に行くリンクを押します。

そこで検索フォームにtestなど検索したい用語を入れて検索すると、

action="/boards"に対してgetで通信を投げるので、BoardsController#indexに割り振られて、のtestと言うキーワードが入ったブックマークしていない掲示板まで取得されてしまう。

これは、BoardsController#indexにルーティングされているからです。

本来ルーティングされてほしいアクションは、BoardsController#bookmarksですが、urlを指定しないだけで変なアクションに割り振られて意図しない挙動をします。

なので、ブックマークした掲示板からではなく、掲示板全体から検索をかけてしまいます。

なのでブックマーク一覧画面で、ブックマークしていない掲示板まで表示されることになります。

ransackによる検索フォームはパーシャルにする

「検索フォーム」は共通で使いまわすようにしておくと便利です。

しかし、検索フォームとは言っても、それぞれがまったく同じ内容のformタグにはならないです

<%= search_form_for @q, url: bookmarks_boards_path do |f| %>
    <%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: '検索ワード' %>
    <%= f.submit '検索', class: 'btn btn-primary' %>
<% end %>

上記の@qurlはパーシャルテンプレートをrenderするファイルによって違います。

なので固定で書いてしまうと汎用性がなくなります。

<%= search_form_for q, url: url do |f| %>
    <%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: '検索ワード' %>
    <%= f.submit '検索', class: 'btn btn-primary' %>
<% end %>

なので、@qurlをローカル変数にして、renderする側で渡してあげるといいです。

**<%=** render 'search_form', q: @q, url: bookmarks_boards_path **%>**

渡すときは上記のような感じです。

これで検索フォームを汎用的にいろんな場所で使いまわすことができます。

search_form_forで指定するurlはrequest.pathではダメ

Railsにはrequestオブジェクトと言うものがあります。

requestオブジェクトには、現在投げているリクエストの情報が入っています。

なので、requestオブジェクトのpathメソッドを使うと、現在アクセスしているパスを取得できます。

他にもrequestオブジェクトが持っているメソッドを使うことで、リクエストに関するいろんな情報を取得できます。

> request.path
=> "/boards"

Railsガイド requestオブジェクト

これを使って、下記のようにsearch_form_forのURL指定することもできます。

**<%=** search_form_for q, url: request.path **do** |f| **%>**

これで現在/productsにリクエストを投げているときに、検索フォームをrenderするなら、request.path/productsになります。

<!-- 検索フォーム -->
<%= render 'search_form', q: @q %>

request.pathを指定すれば、パーシャルテンプレートをrenderするときに、パーシャルテンプレートのローカル変数urlに値を渡す必要がないように感じます。

しかし、request.pathを使ってしまうと、検索用のフォームのaction属性が固定されてしまうので、request.path以外のURLを使いたい場合変更できないです。

そうなると汎用性がないので、request.pathは使わないようにします。

https://pikawaka.com/rails/ransack#ransackの使い方