もふもふ技術部

IT技術系mofmofメディア

【Rails】Gemを使わないFinderオブジェクトを使った検索

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オブジェクトを使ってみるのはいかがでしょうか?