form_withを使って検索機能を追加する
li
= form_with model: @search_articles_form, scope: :q, url: admin_articles_path, method: :get, html: { class: 'form-inline' } do |f|
=> f.select :category_id, Category.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
.input-group
= f.search_field :title, placeholder: "タイトル", class: 'form-control'
span.input-group-btn
= f.submit '検索', class: %w[btn btn-default btn-flat]
今回検索用にform_withを使い検索機能を実装する。
今回はここに著者、タグ、本文の検索機能を追加する。
まず著者、タグ、本文を記入できるようにform_with内に記入していく
li
= form_with model: @search_articles_form, scope: :q, url: admin_articles_path, method: :get, html: { class: 'form-inline' } do |f|
=> f.select :category_id, Category.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
=> f.select :author_id, Author.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
=> f.select :tag_id, Tag.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
.input-group
= f.search_field :title, placeholder: "タイトル", class: 'form-control'
.input-group
= f.search_field :body, placeholder: "本文", class: 'form-control'
span.input-group-btn
= f.submit '検索', class: %w[btn btn-default btn-flat]
form_witn内の
model: @search_articles_form
の部分でSearchArticleFormクラスに以下の情報(category_id,title)を入れてインスタンスを作成するという処理になっていることがわかる。
しかしSearchArticleFormはDBに保存する必要のないクラスのなのでActiveRecordは使うことができない。つまり
$ article.title
などのメソッドは使えなくなる
ActiveRecordの復習
ActiveRecordとは簡単に言えばRubyとDBの翻訳機みたいなものである。
RubyとDBはそれぞれ言語の種類が違うがActiveRecordを使うとその二つをつなぐ役割になる
例えば
$ article.title
とするとActiveRecordによりDBのなかのarticleのtitleが選ばれることになる。
今回はDBが必要ない、つまりActiveRecordが使えず、article.titleなどの便利なメソッドを使うことができない。
ActiveModel
ActiveRecordを使えない(DBを使って処理を行わない)時、便利になるのがActiveModelである。DBを使っていないクラスにActiveModel::Model
をincludeすることでActiveRecordと同じようにコードをかけるというもの。つまりarticle.titleのように便利なメソッドを使うことができる。
ActiveModelの使い方
class SearchArticleForm
include ActiveModel::Model
end
これでarticle.title
などを使える準備は整った。(まだ使えるわけではない)
さらにここからarticle.title
のようなコードのtitle
の部分に何を使えるようにするかを決める今回の場合だとカテゴリー、著者、タグ、タイトル、本文の5つの検索機能が必要なので
SearchArticleFrom
クラスの「カテゴリー、著者、タグ、タイトル、本文」の5つの属性が必要になる。
class SearchArticleForm
include ActiveModel::Model
attr_accessor :category_id, :integer
attr_accessor :author_id, :integer
attr_accessor :tag_id, :integer
attr_accessor :title, :string
attr_accessor :body, :string #body自体はtextだがbodyを探す場合の文字の種類はstringになる
end
attr_accessorを使うことでDBを使わないRubyの属性作ることができる
しかしattr_accessorもActiveModelで書き直すことができる
class SearchArticlesForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :category_id, :integer
attribute :author_id, :integer
attribute :tag_id, :integer
attribute :title, :string
attribute :body, :string
end
ActiveModel::Attributesと記入することでActiveRecordで記入している形式と全く同じ形で書くことができる。
これで
$ SearchArticlesForm.category_id
$ SearchArticlesForm.author_id
$ SearchArticlesForm.tag_id
$ SearchArticlesForm.title
$ SearchArticlesForm.body
といった形でメソッドが使えるようになる
search機能の追加
ActiveModelを使うことができるようになったので、次にcategory_id
author_id
tag_id
title
body
の検索機能を上記のメソッドたちを使ってsearchメソッドを作っていく。
class SearchArticlesForm
def search
relation = Article.distinct
relation = relation.by_category(category_id) if category_id.present?#categoryの検索機能
relation = relation.by_author(author_id) if author_id.present? #authorの検索機能
relation = relation.by_tag(tag_id) if tag_id.present? #tagの検索機能
title_words.each do |word|
relation = relation.title_contain(word) #titleの検索機能
end
body_words.each do |word|
relation = relation.body_contain(word) #bodyの検索機能
end
relation
end
private
def title_words
title.present? ? title.split(nil) : []
end
def body_words
body.present? ? body.split('') : []
end
end
解説① distinct
relation = Article.distinct
discountを使うと重複するArticleを1つにまとめることができる
解説② by_category
relation = relation.by_category(category_id) if category_id.present?
省略せずに記入すると
relation = Article.distinct.by_category(category_id) if category_id.present?
となる
distinctで重複を調べるための範囲をby_category(category_id)で指定している
範囲の指定なのでby_category(category_id)はscopeでの範囲指定であるということがわかるscopeが書いてあるarticle.rbを見てみると
scope :by_category, ->(category_id) { where(category_id: category_id) }
scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }
となっている。
上記のcategoryのスコープは「category_idの範囲」を指定している。
titleのスコープは「wordに入った文字がtitleに含まれている範囲」を指定している。
著者、タグ、本文のスコープも付けないといけないのでつける
scope :by_category, ->(category_id) { where(category_id: category_id) }
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :by_tag, ->(tag_id) { joins(:tags).where(article_tags: { tag_id: tag_id }) }
scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%")
scope :body_contain, ->(word) { joins(:sentences).merge(where('sentences.body LIKE ?', "%#{word}%")) }
by_tagとbody_containの書き方だけ違う。
それはArticle自信が持っている属性(メソッド)によるものである。
Articleはcategoryとauthor、titlesの属性は持っているが、tagとbodyの属性は持っていない。つまりArticle.tagやArticle.bodyはできない。Articleとtagに関しては多対多の関係なのでarticle_tagを介して、bodyはsentenceを介している。
そのためその2つに関してはjoinsを使いscopeをかけるようにしている。
解説③ title_words
title_words.each do |word|
relation = relation.title_contain(word) #titleの検索機能
end
private
def title_words
title.present? ? title.split(nil) : []
end
title_words以下の文は三項演算子であり、このように置き換えられる
def title_words
if title.present?
title.split(nil)
else
[]
end
end
split(nil)で半角スペースのある単語は分割して調べることができる