検索機能の追加

どうもどハマりエンジニアです。

本日はActiceModelでDBに依存しないモデルを作り、検索機能の実装を行っていきます。

 

今回の勉強内容

・form object

・Active Model

・scope

 

 

form objectとは?

form_withのmodelオプション*1にActive Record以外のオブジェクトを渡すデザインパターンです。form_withのmodelオプションに渡すオブジェクト自体もform objectと呼びます。

利点は大きく次の2点です。

  • DBを使わないフォームでも、Active Recordを利用した場合と同じお作法を利用できるので可読性が増す
  • 他の箇所に分散されがちなロジックをform object内に集めることができ、凝集度を高められる

 

Active Modelとは?

Active RecordからDBに依存する部分を除いた振る舞いを提供しているライブラリです。これを利用することにより、DBを利用しないフォームでもActive Recordを利用したときと同じような記述をすることができます。

 

 

モデルの作成

scopeを使い複数のクエリをまとめたメソッドを作成していきます。

また他テーブルとの結合させるためにjoinsを使用。

 

models/article.rb

belongs_to :category
belongs_to :author
has_many :article_tags
has_many :tags, through: :article_tags
has_many :sentences, through: :article_blocks, source: :blockable, source_type: 'Sentence'
 ・
 ・
 ・
scope :by_category, ->(category_id) { where(category_id: category_id) }
scope :by_state, ->(state) { where(state: state) }
scope :by_tag, ->(tag_id) { joins(:tags).where(article_tags: { tag_id: tag_id }) }
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }
scope :body_contain, ->(word) { joins(:sentences).merge(where('sentences.body LIKE ?', "%#{word}%")) }

scopeの上から4つ目まではセレクトで選択できるように作成します。下記二つのscopeは文字列から検索出来るように作成します。

上から3つ目のscopeはfoinsを使いtagsテーブルと結合させています。Active Recordでは、joinsメソッドを使用して関連付けでJOIN句を指定する際に、モデルで定義された関連付けの名前をショートカットとして使用できます。なので今回はアソシエーションを組んだtagsを使用します。今回は中間テーブルが存在しますので、article_tagsテーブルのtag_idでクエリを発行させることで条件の合うデータを取得できます。

同じく最後のscopeでもjoinsを使いsentencesテーブルを結合させます。mergeメソッドは直前に行った条件に対して、さらに絞り込みを行いたい時に使います。(今回はなくても機能します)

 

 

ActiveModelを使いDBに依存しないモデルの作成

app/forms/serch_form_atricles.rb

class SearchArticlesForm
  # ActiveModelはモデルではないがまるでモデルかのように扱うことが出来る優れものである。
  include ActiveModel::Model
  include ActiveModel::Attributes

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

  def search
    # distinctメソッドは重複レコードを1つにまとめるためのメソッド
    relation = Article.distinct
 
    # by_〇〇はモデルのscopeで定義
    relation = relation.by_category(category_id) if category_id.present?
    relation = relation.by_author(author_id) if author_id.present?
    relation = relation.by_state(state) if state.present?
    relation = relation.by_tag(tag_id) if tag_id.present?

    title_words.each do |word|
      relation = relation.title_contain(word)
    end
    body_words.each do |word|
      relation = relation.body_contain(word)
    end
    relation
  end

  private

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

  def body_words
    body.present? ? body.split(' ') :
  end
end

 

 

コントローラーの作成

 

app/controllers/admin/articles_controleller.rb

def index
  authorize(Article)

  @search_articles_form = SearchArticlesForm.new(search_params)
  @articles = @search_articles_form.search.order(id: :desc).page(params[:page]).per(25)
end
 ・
 ・
 ・
def search_params
  params[:q]&.permit(:title, :body, :category_id, :author_id, :state, :tag_id)
end

 

 

テンプレートの作成

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'
  => f.select :state, Article.states.map{ |k, v| [t("enums.article.
     state.#{k}"), v] }, { 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]

pluckは、1つのモデルで使用されているテーブルからカラム (1つでも複数でも可) を取得するクエリを送信するのに使用できます。引数としてカラム名のリストを与えると、指定したカラムの値の配列を、対応するデータ型で返します。今回はnameとidカラム を取得してきて、表示されるのは第一引数、第二引数はパラメーターとして渡されます。