Madogiwa Blog

主に技術系の学習メモに使っていきます。

RubyonRails: 検索フォームから値を受け取って、値があれば検索するときの実装を考えてみる🤔

みなさん、こんばんは。まどぎわです。
今回は検索処理で値があるときだけ絞り込みを行うときにどうやって実装するのが良いのかなと、すこし考えたので検討過程をメモしておきます。

はじめに

今回は、下記のような前提で検索処理を実装することを想定してます。

  • 検索対象はPost、Postにはタイトルと本文がある。
  • Postにはタグが複数紐付いている。タグは、acts-as-taggable-onで実装されている。
  • 検索条件には、キーワード(タイトルの部分一致)とタグがある。
  • それぞれの検索条件が指定されていれば条件で絞り込みを行い、指定されていなければ検索処理を行わない。

まずは何も考えず実装してみる(かなり良くない)

なにも考えず実装すると下記のような感じになりますかね(;・∀・)
自分で書いといてあれですが、ごちゃごちゃしていて辛い・・・。

class PostsController
  def index
    @posts = if search_params[:keyword].present? && search_params[:tag].present?
      Post.where('title LIKE ?', "%#{search_params[:keyword]}%").tagged_with(search_params[:tag])
    elsif search_params[:keyword].present?
      Post.where('title LIKE ?', "%#{search_params[:keyword]}%")
    elsif search_params[:tag].present?
      Post.tagged_with(search_params[:tag])
    else
      Post.all
    end
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

とりあえず検索処理をModelに移してみる(あまり良くない)

Controllerはきれいになり、先程よりは良くなりましたが、ごちゃごちゃした部分がModelに移動しただけで根本的な問題は解決してないですね・・・。

class PostsController
  def index
    @posts = params[:search] ? Post.search(search_params) : Post.all
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

class Post
  scope :search, ->(params) {
    if params[:keyword].present? && params[:tag].present?
      where('title LIKE ?', "%#{params[:keyword]}%").tagged_with(params[:tag])
    elsif params[:keyword].present?
      where('title LIKE ?', "%#{params[:keyword]}%")
    elsif params[:tag].present?
      tagged_with(search_params[:tag])
    else
      all
    end
  }
end

とりあえずModelの検索ロジックをリファクタリングしてみる(うーん)

さっきよりは断然良さそうですが、最初にallで取得しているところとか、毎回変数に入れているところに、少し違和感を感じますね(;・∀・)
※また最初、この書き方だとallwheretagged_withSQLが発行されてしまい非効率だと思ってたんですが、ちゃんと1回のSQLで取得出来るみたいですね、ActiveRecordすごい🙄

class PostsController
  def index
    @posts = params[:search] ? Post.search(search_params) : Post.all
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

class Post
  scope :search, ->(params) {
    posts = all
    posts = posts.where('title LIKE ?', "%#{params[:keyword]}%") if params[:keyword].present?
    posts = posts.tagged_with(params[:tag]) if params[:tag].present?
    posts
  }
end

検索条件ごとにscopeを用意して個別に検索ロジックを実装してみる(良さそう)

今回は、これが良いんじゃないかと思ってるんですが、検索条件別にscopeを用意してあげて、その中で検索条件の有無でallを返すか絞り込みを行った結果を返すか分岐を行っています。
この方法であれば、メソッドチェーンを使って条件を適用出来るため分かりやすい、個別で検索条件を使うときに便利かなと思ったんですが、どうでしょうか?👀

class PostsController
  def index
    @posts = params[:search] ? Post.search(search_params) : Post.all
  end
  
  def search_params
    params.require(:search).permit(:keyword, :tag)
  end
end

class Post
  scope :search, ->(params) {
    search_by_keyword(params[:keyword]).search_by_tag(params[:tag])
  }
  scope :search_by_keyword, ->(keyword) {
    return all if keyword.blank?
    where('title LIKE ?', "%#{keyword}%")
  }
  scope :search_by_tag, ->(tag) {
    return all if tag.blank?
    tagged_with(tag)
  }
end

おわりに

今回は検索ロジックについて、ちょっと考えてみました。
Railsの良い感じの既存メソッドを使って、これよりも良い実装も全然あるような気もするので教えてください(;・∀・)