どうもどハマりエンジニアです。
本日は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カラム を取得してきて、表示されるのは第一引数、第二引数はパラメーターとして渡されます。