【Rails】複数のモデルをフォームオブジェクトを使って保存する

# フォームオブジェクトとは
データベースとは無関係なフォームや複数のモデルを扱うフォームを作るとき
フォームオブジェクトというものを導入すると処理をすっきり書くことができます。
Railsのドキュメントで正式に使用されている言葉ではないですが、よく用いられる手法となります。
`app/forms`フォルダ配下にフォームオブジェクトを置くことが多いです。

# 具体例
以下のようなケースを考えることにします。
- 名前、メールアドレス、郵便番号、住所でサインアップを行う
- 名前とメールアドレスはUserモデルに保存する
- 郵便番号と住所はAddressモデルに保存する
- UserモデルとAddressモデルは1:1のアソシエーションを持つ

![erd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736970/288bda7d-5240-02b6-b17c-8f054ec9e9e4.png)

# ソースコード

## モデル
UserモデルとAddressモデルのアソシエーションとバリデーションを適当に書いています。
```ruby:app/models/user.rb
class User < ApplicationRecord
  has_one :address, dependent: :destroy

  validates :name, presence: true, length: { maximum: 14 }
  validates :email, presence: true, uniqueness: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true
end
```

```ruby:app/models/address.rb
class Address < ApplicationRecord
  belongs_to :user

  POSTAL_CODE_REGEXP = /\A\d{3}-?\d{4}\z/

  validates :address, presence: true, length: { maximum: 50 }
  validates :postal_code, presence: true
  validates :postal_code, format: { with: POSTAL_CODE_REGEXP }, allow_blank: true
end
```

## フォームオブジェクト
```ruby:app/forms/registration_form
class RegistrationForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attr_accessor :user_attributes, :address_attributes

  validate :user_validate
  validate :address_validate

  def save
    return false if invalid?

    ActiveRecord::Base.transaction do
      save!
    end
    true
  rescue ActiveRecord::RecordInvalid
    false
  end

  def save!
    user.save!
  end

  def user
    @user ||= User.new(user_attributes)
  end

  def address
    @address ||= user.build_address(address_attributes)
  end

  private

  def user_validate
    return unless user.invalid?

    user.errors.full_messages.each do |message|
      errors.add(:base, message)
    end
  end

  def address_validate
    return unless address.invalid?

    address.errors.full_messages.each do |message|
      errors.add(:base, message)
    end
  end
end
```
フォームオブジェクトに`name`, `email`, `postal_code`, `address`それぞれのバリデーションを書いてもいいのですが
モデルに書いたバリデーションと全く同じことを書くのも嫌なので今回は`user_validate`, `address_validate`でUserモデル, Addressモデルのバリデーションでチェックを行いエラーメッセージは`errors.add(:base, message)`でそのまま代入しています。

`user.save!`で関連づけられている`address`も同時に保存されます。

※ エラーメッセージを代入する処理は
```ruby
user.errors.full_messages.each do |message|
    errors.add(:base, message)
end
```
の代わりに
```ruby
errors.merge!(user.errors)
```
でも代入できますが、マージする方法だとフォームオブジェクトのエラーメッセージの日本語化ができていないので
下記のようなlocaleファイルを追加してあげてください。
```yml
ja:
  activemodel:
    attributes:
      registration_form:
        name: 名前
        email: メールアドレス
        postal_code: 郵便番号
        address: 住所
```

## views
```erb:app/views/registrations/new/html.erb
<div class="container">
  <h1>新規登録</h1>
  <%= form_with model: @registration_form, url: registrations_path do |f| %>
    <%= render 'shared/error_messages', object: f.object %>

    <%= fields_for f.object.user do |ff| %>
      <div>
        <%= ff.label :name, class: 'form-label' %>
        <%= ff.text_field :name, class: "form-control" %>
      </div>

      <div>
        <%= ff.label :email, class: 'form-label' %>
        <%= ff.text_field :email, class: "form-control" %>
      </div>
    <% end %>

    <%= fields_for f.object.address do |ff| %>
      <div>
        <%= ff.label :postal_code, class: 'form-label' %>
        <%= ff.text_field :postal_code, class: "form-control" %>
      </div>

      <div>
        <%= ff.label :address, class: 'form-label' %>
        <%= ff.text_field :address, class: "form-control" %>
      </div>
    <% end %>

    <%= f.submit '新規登録', class: "btn btn-primary" %>
  <% end %>
</div>
```
`fields_for`でモデル毎に分割してあげるることでパラメーターは下記のような形になります
```
{
    "user"=>{"name"=>"yamada", "email"=>"test@example.com"}, 
    "address"=>{"postal_code"=>"100-0000", "address"=>"千葉県"}
}
```
## コントローラー
```ruby:app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
  def new
    @registration_form = RegistrationForm.new
  end

  def create
    @registration_form = RegistrationForm.new(user_attributes: user_params,
                                         address_attributes: address_params)
    if @registration_form.save
      redirect_to root_path
    else
      render 'new', status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email)
  end

  def address_params
    params.require(:address).permit(:postal_code, :address)
  end
end
```
`params`の内`User`モデルに関するパラメーターは`user_attributes`として渡し、`Address`モデルに関するパラメーターは`address_attributes`としてフォームオブジェクトに渡してあげます。
attributesはフォームオブジェクト内でそれぞれのモデルのnewするときのパラメーターとして使用します。


# まとめ
フォームオブジェクトを使った実装方法について解説しました。
今回はバリデーション処理は既にあるモデルのバリデーションに任せることにしましたが、
```ruby
class RegistrationForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :email, :string
  attribute :postal_code, :string
  attribute :address, :string

  POSTAL_CODE_REGEXP = /\A\d{3}-?\d{4}\z/

  validates :name, presence: true, length: { maximum: 14 }
  validates :email, presence: true, uniqueness: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true
  validates :address, presence: true, length: { maximum: 50 }
  validates :postal_code, presence: true
  validates :postal_code, format: { with: POSTAL_CODE_REGEXP }, allow_blank: true

# 以下略
```
みたいにフォームの項目それぞれ書くこともできると思います。
この場合`views`ファイルで`fields_for`を使う必要もなくなると思います。
（↓こんな感じのパラメーターを渡すイメージです。）
```
{
    "registration_form"=>{
        "name"=>"yamada",
        "email"=>"test@example.com",
        "postal_code"=>"100-0000", 
        "address"=>"千葉県"
    }
}
```

また、`accepts_nested_attributes_for`を使った方法もあると思います。

どれを使うかは一長一短かと思いますがフォームオブジェクトを使う場合
モデルのカラム、バリデーションを全くそのまま使う場合は今回のやり方が簡潔に書けるかなと思います。