23
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rails + ransackでセレクトボックスからscopeを選択して検索条件にする方法

Last updated at Posted at 2022-05-03

はじめに

ransackでよくあるname_contarticles_title_startのような条件では表現できず、scopeを使わないと検索できないケースで、なおかつセレクトボックスから任意の検索条件を選択できるようにする検索フォームを作ってみました。

アプリの仕様

テスト管理アプリケーションです。生徒を表すStudentモデルと、試験の結果を格納するExamモデルがあります。

class Student < ApplicationRecord
  has_many :exams, dependent: :destroy
end
class Exam < ApplicationRecord
  belongs_to :student
end

DBには以下のようにデータが格納されているとします。

studentsテーブル

id name
1 Alice
2 Bob
3 Carol

examsテーブル

id student_id subject score
1 2 English 60
2 2 math 57
3 2 Japanese 48
4 3 English 77
5 3 math 61
6 3 Japanese 72

このとき、各生徒の得点は以下のようになっています。

  • Alice (試験を受けていない)
  • Bob (英語=60、数学=57、国語=48で平均55点)
  • Carol (英語=77、数学=61、国語=72で平均70点)

検索条件

以下の3種類の条件で生徒を検索するのが要件です。また、検索条件はセレクトボックスで選択します。

  • 未受験の生徒(Aliceが該当)
  • 受験済みの生徒(BobとCarolが該当)
  • 平均60点以上の生徒(Carolが該当)

画面イメージはこんな感じです。

z269fMsC8i.gif

開発環境

このサンプルアプリは以下の環境で開発しました。

  • Ruby 3.1.2
  • Rails 7.0.2.4
  • ransack 3.1.0
  • RDBMS: SQLite3

なお、この記事で使ったコードは以下のリポジトリに置いています。

実装例

3つの検索条件をそれぞれStudentモデルのscopeとして定義し、さらにそのスコープを文字列で指定できるセレクトボックス用のscopeを定義して、検索フォーム上のセレクトボックスと関連付けることにしました。

Studentモデル

scopeをあれこれ定義しています。

class Student < ApplicationRecord
  has_many :exams, dependent: :destroy

  # 未受験を検索するscope
  scope :without_exams, -> do
    where(<<~SQL)
      NOT EXISTS (
        SELECT * 
        FROM exams e 
        WHERE e.student_id = students.id
      )
    SQL
  end

  # 受験済みを検索するscope
  scope :with_exams, -> do
    where(<<~SQL)
      EXISTS (
        SELECT * 
        FROM exams e 
        WHERE e.student_id = students.id
      )
    SQL
  end

  # 平均60点以上を検索するscope
  scope :with_good_score, -> do
    ids = Exam.select(:student_id).group(:student_id).having('AVG(score) >= ?', 60)
    where(id: ids)
  end

  # 選択可能なscope名の配列
  SCOPES_FOR_SELECT = %w(without_exams with_exams with_good_score).freeze

  # セレクトボックス用のscope。引数には画面で選択されたscope名が入る
  scope :for_select, -> (scope_name) do
    # 無関係なscopeを指定された場合はエラーとする(セキュリティ対策)
    raise ArgumentError unless scope_name.in?(SCOPES_FOR_SELECT)

    # メタプログラミング(リフレクション)でscopeを呼び出す
    send(scope_name)
  end

  # for_selectのみransackと連携可能なscopeとする
  def self.ransackable_scopes(_auth_object = nil)
    [:for_select]
  end
end

StudentsController

典型的なransackを使ったcontrollerの実装です。特に変わったところはありません。

class StudentsController < ApplicationController
  def index
    @q = Student.order(:id).ransack(params[:q])
    @students = @q.result
  end
end

app/views/students/index.html.erb

検索フォーム部分だけを抜粋します。セレクトボックスの選択項目はStundet::SCOPES_FOR_SELECTから動的に生成しています。

<%= search_form_for @q do |f| %>
  <% options = Student::SCOPES_FOR_SELECT.map { |name| [t(".for_select_#{name}"), name] } %>
  <%= f.select :for_select, options, { include_blank: true } %>
  <%= f.submit '検索' %>
<% end %>

config/locales/en.ymlの内容です。(本来なら英語で書くべきですが、手を抜いて日本語で書いてます😝)

en:
  students:
    index:
      for_select_without_exams: '未受験'
      for_select_with_exams: '受験済み'
      for_select_with_good_score: '平均60点以上'

これで完成です!

z269fMsC8i.gif

まとめ

できあがったコードは結構シンプルですが、ネットを検索しても同じような事例が見つからず、若干試行錯誤したので参考情報として実装例をアップしておきました。
同じような要件に遭遇したら参考にしてみてください!

あわせて読みたい

複雑な検索フォームは無理にransackを使わない、というアプローチもアリだと思います。
以下のスライドではransackを使わない検索フォームの実装例を紹介しています。

23
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?