LoginSignup
9
4

More than 3 years have passed since last update.

rails 学習 ActiveModelを用いた検索機能の実装方法

Posted at

form_withを使って検索機能を追加する

admin/article/index.html.slim
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内に記入していく

admin/article/index.html.slim
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を見てみると

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に含まれている範囲」を指定している。
著者、タグ、本文のスコープも付けないといけないのでつける

article.rb
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)で半角スペースのある単語は分割して調べることができる

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4