form objectを使って複数のActiveRecordを保存する【rails】

はじめに

form objectを使って、一度に複数のActiveRecordを保存する方法について

form objectとは

form objectとはrailsデザインパターンの一つで、もともとはバリデーションなどをformにまとめて、

複数のモデルから実行できるようにすることでコードを簡潔にしたりするもののようです。

他にも、複数のActiveRecordを一度に保存するときに使えます。

今回はこちらについて書いていきます。

前提

Articleモデルの関連

class Article < ApplicationRecord
  belongs_to :category
  belongs_to :author

  has_many :article_tags
  has_many :tags, through: :article_tags

end

articleモデルは、カテゴリーと著者に属していて、タグとは中間テーブルを通して多数対多数の関係にあります。

そして記事一覧ページで、articleモデルのタイトル、カテゴリー、著者、タグでそれぞれ検索できるように、検索フォームを作成します。

記事のタイトルはフリーワード検索、そのほかはセレクトボックスによる検索です。

= 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: 'カテゴリ' }, class: 'form-control'
  => f.select :author_id, Author.pluck(:name, :id) , { include_blank: '著書' }, class: 'form-control'
  => f.select :tag_id, Tag.pluck(:name, :id) , { include_blank: 'タグ' }, class: 'form-control'
  .input-group
    = f.search_field :title, class: 'form-control', placeholder: 'タイトル'
              span.input-group-btn
      = f.submit '検索', class: %w[btn btn-default btn-flat]

記事一覧ページのコントローラー

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

  private

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

form objectの設定方法

まずformsディレクトリを作成して、その配下にファイルを作成します。

$ mkdir app/forms

$ touch app/forms/search_aritcles_form.rb

続いて、classを定義して、ActiveModel::ModelActiveModel::Attributesをインクルードします。

class SearchArticlesForm
  include ActiveModel::Model
  include ActiveModel::Attributes
end

ActiveModel::Modelをインクルードすると、このクラスを擬似モデルのように扱うことができるようになります。

ActiveModel::Attributesをインクルードすると、attributeメソッドが使えるようになります。

これはattr_accesorと同じように定義した属性を使えるようになります。

早速定義します。

class SearchArticlesForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :category_id, :integer
  attribute :author_id, :integer
  attribute :tag_id, :integer
  attribute :title, :string

end

続いて、カテゴリー、著者、タグのセレクトボックスの検索処理と、

記事タイトルのフリーワード検索の処理を書いていきます。

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

  def search
    relation = Article.distinct

    relation = relation.by_category(category_id) if category_id.present?
    relation = relation.by_author(author_id) if author_id.present?
    relation = relation.by_tag(tag_id) if tag_id.present?

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

  private

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

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

上記で使うscopeを定義します。

class Article < ApplicationRecord
  belongs_to :category
  belongs_to :author

  has_many :article_tags
  has_many :tags, through: :article_tags

  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(:article_tags).merge(ArticleTag.where(tag_id: tag_id)) }
# 上記とイコール
# scope :by_tag, ->(tag_id) { joins(:article_tags).where(article_tags: { tag_id: tag_id }) }
  scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }

end

これで下記のような検索機能が実装できました。

f:id:study-output:20210517170251p:plain