【初心者】フォームオブジェクト(フォームモデル)
状況
- rakeタスクで使うために複雑なメソッドを検討していた際に「フォームオブジェクトを検討すべきでは」とアドバイスいただきました
- フォームオブジェクトとはそもそも何なのかを調べてみました
内容
フォームオブジェクトとは?
- フォームオブジェクト(フォームモデル)とは、Railsにおけるデザインパターンの一種
- 複数のモデルにまたがるデータを扱うフォームのバリデーションや処理を、1つのオブジェクトにまとめるためのクラス
なぜフォームモデルを使うのか?
- 通常のActiveRecordのモデル(User, Postなど)は、1つのテーブルに対応する
ご参考
- しかし、以下のようなケースでは1つのモデルでは対応しづらくなる
- ユーザー登録時に'User'と'Profile'を同時に作成したい
- 記事投稿時に Article と Tag を同時に処理したい
- データベースに保存しない入力フォームを作成したい(例: 問い合わせフォーム)
- こうしたケースでは、フォームモデル(フォームオブジェクト)を使うことで、コードの整理がしやすくなり、Fat Controller(太りすぎたコントローラー)を防ぐことができる
フォームモデルの実装方法
User
と Profile
を同時に登録するフォームモデル
例: 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.transaction
で User
と Profile
を保存
📍 どちらかの保存に失敗したらロールバック(データの不整合を防ぐ)
(4) 処理結果に応じたリダイレクト
✅ 成功した場合
➡ redirect_to root_path
でトップページへリダイレクト
❌ 失敗した場合(バリデーションエラーなど)
➡ render :new
でエラーメッセージを表示し、再入力できる
フォームモデルの応用例
-
ログインフォーム(Session 管理)
-
User
とは別に、LoginForm
というフォームモデルを作成し、ログイン処理をカプセル化
-
-
検索フォーム
-
SearchForm
を作成し、クエリパラメータをバリデーションしつつ、適切な検索を実行
-
-
複雑なユーザー設定更新
-
UserSettingForm
を作り、User
とNotificationSetting
などをまとめて更新
-
日常の例を使った説明
普通のモデルの場合
ケーキを注文するために、以下の情報を記入するフォームがある:
- あなたの名前
- ケーキの種類
- 配送先の住所
通常は「顧客モデル」と「注文モデル」の2つがあれば十分(それぞれが簡単な情報を管理するから)
フォームオブジェクトが必要な場合
しかし、こんなケースを考えてみる:
- 注文には「ケーキの種類」だけでなく「ラッピングの指定」や「特別なメッセージ」も含まれる
- 顧客情報には「配送先住所」以外に「請求先住所」が必要
この場合、1つのフォームで「顧客情報」や「注文の詳細」をまとめて扱いたくなるが、これをそのままモデルに詰め込むとコードが読みにくくなる
フォームオブジェクトの仕組み
フォームオブジェクトは、「注文用紙の仲介者」のようなもの
- 実際のケーキ屋さんでは、お客様が注文用紙に記入した内容を店員が「顧客リスト」と「注文リスト」に分けて記録する
- フォームオブジェクトも同じように、「入力内容を整理して、それぞれのモデルに分けて保存する役割」を果たす
まとめ
- FormObject は 「フォームの責務を切り出す」 ことに特化したクラス
- 複数のモデルにまたがるフォームデータの管理
- 既存のモデルの肥大化を防ぎ、Fat Modelを回避
- モデルとUI(フォーム)の乖離を防ぎ、メンテナビリティを向上
- データの整合性を担保するためのカスタムバリデーションの追加
- フォームデータの前処理(例: フォーマット変換など)
- モデル間の関連を考慮しつつ、適切なタイミングでデータを保存す
上記のようなケースで役に立つ
所見
- とにかくコントローラーは薄く が重要なのだと分かりました
- フォームが複雑になったらフォームオブジェクト!を今後意識してまいりたいです
Discussion