検索機能の追加
実装したいこと
・著者、タグ、コンテンツ(記事内容)に関しても検索が行えるようにする
・著者、タグはセレクトボックスによる選択、コンテンツ(記事内容)はフリーワード検索が行えるようにする
・追加する各検索機能では、下書き状態の記事も検索できるように実装
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カラムで検索していると理解しました。
参考文献