🍰

【初心者】フォームオブジェクト(フォームモデル)

2025/02/09に公開

状況

  • rakeタスクで使うために複雑なメソッドを検討していた際に「フォームオブジェクトを検討すべきでは」とアドバイスいただきました
  • フォームオブジェクトとはそもそも何なのかを調べてみました

内容

フォームオブジェクトとは?

  • フォームオブジェクト(フォームモデル)とは、Railsにおけるデザインパターンの一種
  • 複数のモデルにまたがるデータを扱うフォームのバリデーションや処理を、1つのオブジェクトにまとめるためのクラス

なぜフォームモデルを使うのか?

  • 通常のActiveRecordのモデル(User, Postなど)は、1つのテーブルに対応する
    ご参考

https://zenn.dev/tatsuki_otake/articles/1e35af405d9455

  • しかし、以下のようなケースでは1つのモデルでは対応しづらくなる
    • ユーザー登録時に'User'と'Profile'を同時に作成したい
    • 記事投稿時に Article と Tag を同時に処理したい
    • データベースに保存しない入力フォームを作成したい(例: 問い合わせフォーム)
  • こうしたケースでは、フォームモデル(フォームオブジェクト)を使うことで、コードの整理がしやすくなり、Fat Controller(太りすぎたコントローラー)を防ぐことができる

フォームモデルの実装方法

例: UserProfile を同時に登録するフォームモデル

1. フォームモデルの作成
通常の ActiveRecord モデルとは異なり、ActiveModel::Model を include する

# app/forms/user_registration_form.rb
class UserRegistrationForm
  include ActiveModel::Model

  # フォームで扱う属性を定義
  attr_accessor :name, :email, :password, :profile_bio, :profile_avatar

  # バリデーションを定義
  validates :name, presence: true
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 6 }
  validates :profile_bio, length: { maximum: 500 }

  def save
    return false unless valid? # バリデーションが通らなければ保存しない

    # トランザクションで `User` と `Profile` を作成
    ActiveRecord::Base.transaction do
      user = User.create!(name: name, email: email, password: password)
      Profile.create!(user: user, bio: profile_bio, avatar: profile_avatar)
    end

    true
  rescue ActiveRecord::RecordInvalid
    false
  end
end

2. コントローラーでフォームモデルを使用
コントローラーの create メソッドでフォームモデルを活用する

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @form = UserRegistrationForm.new
  end

  def create
    @form = UserRegistrationForm.new(user_params)

    if @form.save
      redirect_to root_path, notice: 'ユーザー登録が完了しました'
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user_registration_form).permit(:name, :email, :password, :profile_bio, :profile_avatar)
  end
end

3. ビューでフォームモデルを利用
フォームオブジェクトを form_with で扱う

<!-- app/views/users/new.html.erb -->
<%= form_with model: @form, url: users_path do |f| %>
  <div>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div>
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

  <div>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>

  <div>
    <%= f.label :profile_bio, 'プロフィール' %>
    <%= f.text_area :profile_bio %>
  </div>

  <div>
    <%= f.submit '登録' %>
  </div>
<% end %>

📌 フォームオブジェクトを使ったときのアクションやビューの動き

  • 上記 UserRegistrationForm を使った実装では、「ユーザーの新規登録」 を行うフォームを作成
  • このフォームを通じて User(ユーザー)と Profile(プロフィール)の両方を一度に作成 可能

📌 具体的な動作の流れ

(1) ユーザーが新規登録フォームを開く
📍 UsersController#new が呼ばれ、フォームオブジェクト UserRegistrationForm.new が作成される
📍 ビューに @form を渡し、form_with によってフォームが表示される

(2) フォームにデータを入力し、送信する
📍 ユーザーが名前・メールアドレス・パスワード・プロフィール情報を入力し、送信
📍 UsersController#create が呼ばれ、UserRegistrationForm にデータを渡す

(3) フォームオブジェクトの save メソッドが実行
📍 save メソッドでバリデーションチェック
📍 問題なければ ActiveRecord::Base.transactionUserProfile を保存
📍 どちらかの保存に失敗したらロールバック(データの不整合を防ぐ)

(4) 処理結果に応じたリダイレクト
成功した場合
redirect_to root_path でトップページへリダイレクト
失敗した場合(バリデーションエラーなど)
render :new でエラーメッセージを表示し、再入力できる

フォームモデルの応用例

  1. ログインフォーム(Session 管理)

    • User とは別に、LoginForm というフォームモデルを作成し、ログイン処理をカプセル化
  2. 検索フォーム

    • SearchForm を作成し、クエリパラメータをバリデーションしつつ、適切な検索を実行
  3. 複雑なユーザー設定更新

    • UserSettingForm を作り、UserNotificationSetting などをまとめて更新

日常の例を使った説明

普通のモデルの場合

ケーキを注文するために、以下の情報を記入するフォームがある:

  1. あなたの名前
  2. ケーキの種類
  3. 配送先の住所

通常は「顧客モデル」と「注文モデル」の2つがあれば十分(それぞれが簡単な情報を管理するから)

フォームオブジェクトが必要な場合

しかし、こんなケースを考えてみる:

  • 注文には「ケーキの種類」だけでなく「ラッピングの指定」や「特別なメッセージ」も含まれる
  • 顧客情報には「配送先住所」以外に「請求先住所」が必要

この場合、1つのフォームで「顧客情報」や「注文の詳細」をまとめて扱いたくなるが、これをそのままモデルに詰め込むとコードが読みにくくなる

フォームオブジェクトの仕組み

フォームオブジェクトは、「注文用紙の仲介者」のようなもの

  • 実際のケーキ屋さんでは、お客様が注文用紙に記入した内容を店員が「顧客リスト」と「注文リスト」に分けて記録する
  • フォームオブジェクトも同じように、「入力内容を整理して、それぞれのモデルに分けて保存する役割」を果たす

まとめ

  • FormObject は 「フォームの責務を切り出す」 ことに特化したクラス
    • 複数のモデルにまたがるフォームデータの管理
    • 既存のモデルの肥大化を防ぎ、Fat Modelを回避
    • モデルとUI(フォーム)の乖離を防ぎ、メンテナビリティを向上
    • データの整合性を担保するためのカスタムバリデーションの追加
    • フォームデータの前処理(例: フォーマット変換など)
    • モデル間の関連を考慮しつつ、適切なタイミングでデータを保存す

上記のようなケースで役に立つ

所見

  • とにかくコントローラーは薄く が重要なのだと分かりました
  • フォームが複雑になったらフォームオブジェクト!を今後意識してまいりたいです

Discussion