😀

Railsフォームオブジェクトを活用した複数モデルの処理

2024/12/31に公開

フォームオブジェクトは、複数のモデルを扱うための設計パターンです。コントローラやモデルに複雑なロジックを記述せずコードを整理することができます。
下記に、フォームオブジェクトを使用して複数モデルの保存処理のコード例を記載します。

今回のシナリオ

注文作成を今回のシナリオとします。
ユーザーが複数の商品を購入する際に、下記を同時に処理したい場合があると思います。

  • 注文情報の保存 (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_modelscollect_errors

  • validate_modelsで、OrderOrderItemを一例としてバリデーションを行い、エラー内容を収集します
  • 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