Railsで検索を実装するとき、どのように実装しますか?
ransack などのGemを使っていますか?
今回は、Gemを使わずにFinderオブジェクトを使って検索を実装してみます。
Finderオブジェクトを使うと、モデルとコントローラーから検索のロジックを切り離すことができます。
事前準備
1. モデルを作る
$ rails g model post
20241117000000_posts.rb
class CreatePosts < ActiveRecord::Migration[7.1] def change create_table :posts do |t| t.references :category t.string :name t.string :content t.timestamps end end end
20241118000000_categories.rb
class CreateCategories < ActiveRecord::Migration[7.1] def change create_table :categories do |t| t.string :name t.timestamps end end end
2. マスターデータを用意する
以下のようなカテゴリーを用意します。
ID | カテゴリー名 |
---|---|
1 | 野菜 |
2 | 果物 |
3 | お肉 |
4 | 穀物 |
実装する
1. Formオブジェクトを作る
app/forms/search_posts_form.rb
class SearchPostsForm include ActiveModel::Model include ActiveModel::Attributes attribute :keyword, :string attribute :category, :string, default: :none end
2. Finderオブジェクトを作る
app/finders/base_finder.rb
class BaseFinder def initialize(q) @q = q end private def like_search_condition(words) words.map{ |word| ("%#{ActiveRecord::Base.sanitize_sql_like(word)}%") } end def split_freewords(keyword) keyword.split(/\,| | |\、|,/) end end
app/finders/posts_finder.rb
class PostsFinder < BaseFinder def initialize(q) @record = Post.all super(q) end def execute search_keyword search_category @record end private def search_keyword return if @q.keyword.blank? words = like_search_condition(split_freewords(@q.keyword)) @record = @record.where("name like ? or content like ?", words, words) end def search_category return if @q.category.blank? || @q.category == 'none' category = Category.find_by(id: @q.category) @record = @record.where(category_id: category.id) end end
3. ルーティングを設定する
config/routes.rb
resources :search, only: [:index]
4. コントローラーを作る
app/controllers/search_controller.rb
class SearchController < ApplicationController def index @q = SearchPostsForm.new(search_post_params) @posts = PostsFinder.new(@q).execute end private def search_post_params params.fetch(:q, {}).permit(:keyword, :category) end end
5. viewsを作る
app/views/search/index.html.erb
<%= form_with model: @q, scope: :q, url: search_index_path, method: :get, local: true do |f| %> <div> <%= f.search_field :keyword, value: @q.keyword, placeholder: "キーワードで検索" %> </div> <div> <%= f.collection_select :category, Category.all, :id, :name, value: @q.category, include_blank: true %> </div> <div> <%= f.submit '検索' %> </div> <% end %> <% @posts.each do |post| %> <div><%= post.name %></div> <% end %>
これで実装は終わりです。
他にも検索条件を追加したい場合は、Finderオブジェクトにメソッドを追加していきましょう!
終わり
検索のロジックをFinderオブジェクトに切り離すことで、モデルとコントローラーが太ることなく実装できました。
Gemを使わずに検索を実装するときは、Finderオブジェクトを使ってみるのはいかがでしょうか?