FormObject

FormObjectについて学んだのでまとめてみます。

FormObjectとは

form_withのmodelにActiveRecord以外のオブジェクトを渡すデザインパターン(設計手法)です。
メリットとしては
・DBを使わないフォームでも、ActiveRecordを利用した場合と同じお作法を利用できるので可読性が増す
・他の箇所に分散されがちなロジックをFormObject内に集めることができる
などがあるようです。

RailsでDBのレコードにアクセスする際、生のSQLではなくActiveRecordが提供するクエリメソッドを使います。
User.allとかUser.find(1)とかですね。
これはモデルクラスがApplicationRecordを、ApplicationRecordがActiveRecord::Baseを継承しているから使えるものです。
ではモデルに紐づかないクラス、つまりActiveRecordを継承しないクラスを作りたい時はどうするか。
DBに保存する必要はないがActiveRecordの機能は使いたい、という場合にどうするか、記事の検索フォームを例に見てみます。

検索フォーム

# view

= form_with model: @search_articles_form, scope: :q, url: admin_articles_path, method: :get, html: { class: 'form-inline' } do |f|
  => f.select :author_id, Author.pluck(:name, :id) , { include_blank: '著者' }, class: 'form-control'
  .input-group
    = f.search_field :title, class: 'form-control', placeholder: 'タイトル'
# controller

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, :author_id)
end

検索のロジックを書くapp/forms/search_article_form.rbをいうファイルを作成し、その中でActiveModel::Modelをincludeします。
コントローラーから切り出すことでFatControllerを防ぎます。

class SearchArticlesForm
  include ActiveModel::Model # これ
  include ActiveModel::Attributes

  attribute :author_id, :integer
  attribute :title, :string

  def search
    relation = Article.distinct

    relation = relation.by_author(author_id) if author_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
end
# model

class Article < ApplicationRecord
# 略
  scope :by_author, ->(author_id) { where(author_id: author_id) }
# 略

ActiveModelActiveRecordの中でデータベースに関連する部分以外の機能を切り出したモジュールです。
これをincludeすることでActiveRecordを継承していないクラスでもActiveRecordの便利な機能が使えるようになります。
また、include ActiveModel::AttributesとありますがこれはActiveRecordにあるAttribute APIという機能を使うためのものです。
Attribute APIActiveRecordを操作する際にクラス属性の型を意識しなくても、指定の型へ変換してくれています。

Image from Gyazo

このようにid、created_atを文字列で渡しましたが型が変換されています。
このActiveModel::Attributesをincludeしてattribute :属性, 型とすることで属性、型を定義できます。

最後に

FormObjectをうまく使うとコントローラの肥大化を防げそうです。
何でもかんでもコントローラに詰め込まず、上手に切り出して可読性の高いコードを書けるようになりたいです。

参考サイト

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

【Rails】FormObject で Controller を綺麗に - stmn tech blog

ActiveModel とActiveModel::Attributes について - WeBlog

Active Model の基礎 - Railsガイド

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

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