みなさん、こんばんは。まどぎわです。
今回は検索処理で値があるときだけ絞り込みを行うときにどうやって実装するのが良いのかなと、すこし考えたので検討過程をメモしておきます。
- はじめに
- まずは何も考えず実装してみる(かなり良くない)
- とりあえず検索処理をModelに移してみる(あまり良くない)
- とりあえずModelの検索ロジックをリファクタリングしてみる(うーん)
- 検索条件ごとにscopeを用意して個別に検索ロジックを実装してみる(良さそう)
- おわりに
はじめに
今回は、下記のような前提で検索処理を実装することを想定してます。
- 検索対象は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で取得しているところとか、毎回変数に入れているところに、少し違和感を感じますね(;・∀・)
※また最初、この書き方だとall
とwhere
とtagged_with
でSQLが発行されてしまい非効率だと思ってたんですが、ちゃんと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の良い感じの既存メソッドを使って、これよりも良い実装も全然あるような気もするので教えてください(;・∀・)