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::Model
とActiveModel::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
これで下記のような検索機能が実装できました。