😀
Railsフォームオブジェクトを活用した複数モデルの処理
フォームオブジェクトは、複数のモデルを扱うための設計パターンです。コントローラやモデルに複雑なロジックを記述せずコードを整理することができます。
下記に、フォームオブジェクトを使用して複数モデルの保存処理のコード例を記載します。
今回のシナリオ
注文作成を今回のシナリオとします。
ユーザーが複数の商品を購入する際に、下記を同時に処理したい場合があると思います。
- 注文情報の保存 (Order)
- 注文に含まれる商品の情報 (OrderItem)
フォームオブジェクトのコード例
モデルの定義
# app/models/order.rb
class Order < ApplicationRecord
has_many :order_items, dependent: :destroy
validates :customer_id, :order_date, :total_amount, presence: true
end
# app/models/order_item.rb
class OrderItem < ApplicationRecord
belongs_to :order
validates :product_id, :quantity, presence: true
end
フォームオブジェクトの実装例
class OrderForm
include ActiveModel::Model
attr_accessor :customer_id, :order_date, :total_amount, :items
validates :items, presence: true
validate :validate_models
def save
return false unless valid?
ActiveRecord::Base.transaction do
order = Order.create!(customer_id: customer_id, order_date: order_date, total_amount: total_amount)
items.each do |item|
order.order_items.create!(product_id: item[:product_id], quantity: item[:quantity])
end
end
true
rescue ActiveRecord::RecordInvalid
false
end
private
def validate_models
order = Order.new(customer_id: customer_id, order_date: order_date, total_amount: total_amount)
collect_errors(order)
items.each do |item|
order_item = OrderItem.new(product_id: item[:product_id], quantity: item[:quantity])
collect_errors(order_item)
end
end
def collect_errors(model)
return if model.valid?
model.errors.each do |attribute, message|
errors.add(attribute, message)
end
end
end
バリデーション
-
validate :validate_models
は、モデル側のバリデーションをフォームの一部として実行します
save
メソッド
-
valid?
でフォーム全体の有効性を確認します -
ActiveRecord::Base.transaction
で、注文と商品情報を一括で保存します(エラー発生時はロールバックされます)
validate_models
とcollect_errors
-
validate_models
で、Order
とOrderItem
を一例としてバリデーションを行い、エラー内容を収集します -
collect_errors
はモデル側のエラーをフォームのエラーに統一して追加します
コントローラの実装例
class OrdersController < ApplicationController
def new
@order_form = OrderForm.new
end
def create
@order_form = OrderForm.new(order_params)
if @order_form.save
redirect_to orders_path, notice: '注文が作成されました'
else
render :new
end
end
private
def order_params
params.require(:order_form).permit(:customer_id, :order_date, :total_amount, items: [:product_id, :quantity])
end
end
フォームオブジェクトのメリット
- 責務の分離
- バリデーションはモデル側で定義、処理の統合はフォームオブジェクト側で行う
- コードの保存性向上
- コントローラのコード量が減り、ロジックが明確化される
- エラー処理の一元化
- フォーム全体のエラーを統一的に解釈できる
Discussion