Ryota400’s blog

エンジニアを目指して書いてます。

検索機能の追加

実装したいこと

・著者、タグ、コンテンツ(記事内容)に関しても検索が行えるようにする
・著者、タグはセレクトボックスによる選択、コンテンツ(記事内容)はフリーワード検索が行えるようにする
・追加する各検索機能では、下書き状態の記事も検索できるように実装

Image from Gyazo

ActiveRecordとは

データベースとのやりとりを行うための機能を提供します。
ActiveRecordでは、RDBのテーブルに対応するモデルクラスにアプリケーションのデータとロジックを実装していきます。

データの永続化に関する処理の多くをこのActiveRecordが担当します。

ActiveModel::Modelとは

ActiveRecordのデータベースに関連する部分以外の機能を切り出したもので、
これを利用することで、DBを利用しないフォームでもActive Recordを利用したときと同じような記述をすることができます

ActiveModel::Attributesとは

ActiveAttributesを利用することで、ActiveRecordと同じように属性の型の指定と変換を行うものです。

FormObjectとは

form_withのmodelオプションにActive Record以外のオブジェクトを渡すパターンのことです。(今回の場合ActiveModelなど)
単体のモデルに依存しない場合や
フォーム専用の特別な処理をモデルに書きたくないときに使われます。

メリット
・DBを使わないフォームでも、Active Recordを利用した場合と同じお作法を利用できるので可読性が増す

・他の箇所に分散されがちなロジックをform object内に集めることができ、凝集度を高められる

Viewの設定

app/views/admin/articles/index.html.slim

= 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, class: 'form-control', placeholder: 'タイトル'
            .input-group
              = f.search_field :body, class: 'form-control', placeholder: "本文"
              span.input-group-btn
                = f.submit '検索', class: %w[btn btn-default btn-flat]

フォームに入力された値がSearchArticleFormクラスに入り、インスタンスを生成するという形になっている。

コントローラの設定

app/controllers/admin/articles_controller.rb カテゴリーの追加

private

def search_params
    params[:q]&.permit(:title, :category_id, :author_id, :body, :tag_id)
  end

Formの設定

検索のロジックを書くapp/forms/search_article_form.rbをいうファイルを作成し、その中でActiveModel::Modelをincludeします。 コントローラーから切り出すことでFatControllerを防ぐ。

app/forms/search_article_form.rb

class SearchArticlesForm
  include ActiveModel::Model #ActiveRecodeと同じようにコードがかけるようになる。
  include ActiveModel::Attributes

#ActiveModel::Attributesで属性を定義する。
  attribute :category_id, :integer
  attribute :author_id, :integer
  attribute :tag_id, :integer
  attribute :title, :string
  attribute :body, :string

  def search
    relation = Article.distinct

    relation = relation.by_category(category_id) if category_id.present?
    title_words.each do |word|
      relation = relation.title_contain(word)
    end
    relation = relation.by_author(author_id) if author_id.present?
    relation = relation.by_tag(tag_id) if tag_id.present?
    body_words.each do |word|
      relation = relation.body_contain(word)
    end
#relationを記載しないと上記の部分で評価されたものが戻り値になる。
#結果として[]が戻り値になる。なのでrelationを記載し最後に評価することで、relationが戻り値となるようにする。

    relation
  end

  private

  def title_words
    title.present? ? title.split(nil) : []
  end

  def body_words
    body.present? ? body.split(nil) : []
  end
end

scope

<使用方法>

class モデル名 < ApplicationRecord
  scope :スコープ名, -> { 条件式 }
end

ここで定義した「スコープ名」をarticle.スコープ名のようにメソッドとして使える。

article.rb

scope :by_category, ->(category_id) { where(category_id: category_id) }
scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :by_tag, ->(tag_id) { joins(:article_tags).merge(ArticleTag.where(tag_id: tag_id)) }
scope :body_contain, ->(word) { joins(:sentences).merge(where('sentences.body LIKE ?', "%#{word}%")) }
#公開状態でも下書き状態でも検索できるようにするため
sentencesテーブルを結合させて、sentencesテーブルのbodyカラムで検索していると理解しました。

参考文献

tanakanoblogdesu.hatenablog.com

ログイン - はてな