ActiveModelとFormObject

[ActiveModelとFormObject]

ActiveModelは、ActiveRecordを継承しないクラスでもActiveRecordと同じメソッド(validatesなど)が使えるようになるもの(ActiveRecordのDBに関係する部分を除いたライブラリ)
ActiveModelはFormObjectで使われることが多い
FormObjectは、form_withのmodelオプションにActive Record以外のオブジェクトを渡すもの
FormObjectを利用するメリットは、以下の2つです
・DBを使わないフォームでもActive Recordを利用した時の[form_with model: ○○]の形式で記述でき、コントローラの中の記述も同じような記述で記載できるので、可読性を高くできる
・モデルと1対1で紐づかないようなフォームで、他の箇所に分散されるロジックをFormObject内に集めることができる

FormObjectでActiveModelを利用した例を以下に示します

[app/views ファイル]

<%= form_with model: @search_articles_form, scope: :q, url: admin_articles_path 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' %>
<%= f.search_field :body, class: 'form-control', placeholder: '記事内容' %>
<%= f.search_field :title, class: 'form-control', placeholder: 'タイトル' %>
<%= f.submit '検索', class: "btn btn-default btn-flat" %>
[app/controllers ファイル]

def index
  @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(:category_id, :author_id, :tag_id, :body, :title)
end
[app/forms ファイル]

  include ActiveModel::Modelinclude ActiveModel::Attributes          ⑤

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

  def search       ③
    relation = Article.distinct

    title_words.each do |word|
      relation = relation.title_contain(word)
    end
    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?
    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(nil) : []
  end
[app/models/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).where(article_tags: { tag_id: tag_id }) }
  scope :body_contain, ->(body) { joins(:sentences).merge(where('sentences.body LIKE ?', "%#{body}%")) }

①でActiveModelのインスタンスを作成(検索条件のインスタンス)
②でActiveModelのインスタンスメソッドを使っている(searchメソッド)
③のsearchメソッドでは、受け取った検索でArticleから検索を行なっている
④は、ActiveModelを使う時に記述する
⑤は、ActiveModelを利用した際のデータの型変換を定義する際に記述(型変換は、⑥で記述している)
⑥は、attributeに属性名と型を渡すことにより、属性が使えるようになる

参考記事:

form objectを使ってみよう - メドピア開発者ブログ

ActiveModel::Attributes が最高すぎるんだよな。 - Qiita

【Rails】 便利なpluckメソッドをマスターしよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

【Ruby on Rails】Active Recordの絞り込みメソッドまとめ | プログラミングマガジン

Formオブジェクト~1つのフォームで複数モデルとやりとりをする画期的なヤツ~ - Qiita

ActiveModel::Attributesを使う - Qiita

【Rails】「ActiveModel::Attributes」が便利という話 - 日々の学びのアウトプットするブログ

【Ruby】配列の中のハッシュの中から、キーワード検索する - Qiita

【Rails】 joinsメソッドのテーブル結合からネストまでの解説書 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

【Rails入門】where likeであいまい検索!複数条件やOR条件も解説 | 侍エンジニアブログ

Railsのポリモーフィック関連 初心者→中級者へのSTEP10/25 - Qiita