1つのフォームから複数のテーブルに保存する方法

# はじめに
1つのフォームから複数のテーブルに保存する方法を記録に残します。
今回は、寄付金額と寄付者の住所を一つのフォームから入力させ、
それぞれdonationテーブルとaddressテーブルの2テーブルに保存したいとします。

## 1. 前提
以下の通り住所と寄付金額を入力するフォームを作成したとします。

    <%= form_with url: donations_path, local: true do |f| %>
      <h1>住所</h1>
      <div class="field">
       <%= f.label :postal_code, "郵便番号（ハイフンを含む）" %>
       <%= f.text_field :postal_code %>
      </div>
      <div class="field">
        <%= f.label :prefecture, "都道府県" %>
        <%= f.collection_select :prefecture, Prefecture.all, :id, :name %>
      </div>
      <div class="field">
        <%= f.label :city, "市町村（任意）" %>
        <%= f.text_field :city %>
      </div>
      <div class="field">
        <%= f.label :house_number, "番地（任意）" %>
        <%= f.text_field :house_number %>
      </div>
      <div class="field">
        <%= f.label :building_name, "建物名（任意）" %>
        <%= f.text_field :building_name %>
      </div>
    
      <h1>寄付</h1>
      <div class="field">
        <%= f.label :price, "金額（¥1~1,000,000）" %>
        <%= "¥" %>
        <%= f.text_field :price %>
      </div>
    
      <div class="actions">
        <%= f.submit "寄付する" %>
      </div>
    <% end %>

このままparams内に格納されているデータを見ると、以下の通り表示される。
1つのハッシュ内に2テーブルに関する情報が格納されていることがわかる。

    [1] pry(#<DonationsController>)> params
    => <ActionController::Parameters {"authenticity_token"=>"9ap7qH1l9Ag6xdRnvaRNe9ssuypIZwyM25mtzBwg+T1xrpOlu8+d0lSkoc1sI0l6QlAAQdzmgFCpOw9ekDE83Q==", "postal_code"=>"146-0081", "prefecture"=>"13", "city"=>"", "house_number"=>"", "building_name"=>"", "price"=>"100", "commit"=>"寄付する", "controller"=>"donations", "action"=>"create"} permitted: false>

## 2. 一つ一つのパラメーターを丁寧に取り出す記述する方法
    # 省略 
    def create
        @donation = Donation.create(donation_params)
        Address.create(address_params)
        redirect_to root_path
      end
    
      private
    
      def donation_params
        params.permit(:price).merge(user_id: current_user.id)
      end
    
      def address_params
        params.permit(:postal_code, :prefecture, :city, :house_number, :building_name).merge(donation_id: @donation.id)
      end
    end

しかし、この実装にはいくつかの重大な課題があります。

- それぞれのモデルにバリデーション設定がされている場合、一度のフォーム送信で、複数のモデルに設置されたバリデーションを通過する必要があります。
- 送信した内容がバリデーションを通過せず保存できなかった場合、複数のモデルについてのエラーメッセージを表示しなければなりません。

これは複雑かつ非効率な記述になってしまいます。
複数のモデルのインスタンスをコントローラーやビューで扱うためです。

この問題を解決する方法の一つに<strong>Formオブジェクトパターン</strong>というものがある。

## 3. Formオブジェクトパターンを用いた実装
- <strong>Formオブジェクトパターン</strong>
1つのフォーム送信で複数のモデルを操作したい場合や、テーブルに保存しない情報にバリデーションを設定したい場合に使います。
つまり、モデルに近しい機能を持ったクラスを新たに作成し、そのクラスに複数のテーブルへ保存させる処理やバリデーションを記述する、という方法です。
すなわち、newアクションで生成したインスタンスをform_withのmodelオプションに指定することや、インスタンスに対してバリデーションを実行することができます。

Formオブジェクトは、以下の手順で実装します。

- 手順1
新たにmodelsディレクトリ直下にファイルを作成し、クラスを定義する
- 手順2
作成したクラスにform_withメソッドに対応する機能とバリデーションを行う機能を持たせる
- 手順3
保存したい複数のテーブルのカラム名全てを属性値として扱えるようにする
- 手順4
バリデーションの処理を書く
- 手順5
データをテーブルに保存する処理を書く
- 手順6
コントローラーのnewアクション、createアクションでFormオブジェクトのインスタンスを生成するようにする
- 手順7
フォーム作成の部分をFormオブジェクトのインスタンスを引数として渡す形に変更する

      # Formオブジェクトのイメージ図
      class DonationAddress
          include ActiveModel::Model
          attr_accessor :postal_code, :prefecture, :city, :house_number, :building_name, :price, :user_id
        
          # ここにバリデーションの処理を書く
        
          def save
            # 各テーブルにデータを保存する処理を書く
          end
      end

- ActiveModel::Model
クラスにActiveModel::Modelをincludeすると、そのクラスのインスタンスはActiveRecordを継承したクラスのインスタンスと同様に form_with や render などのヘルパーメソッドの引数として扱え、バリデーションの機能を使用できるようになります。
Formオブジェクトパターンを実装するためにActiveModel::Modelをincludeしたクラスのことを「Formオブジェクト」と呼ぶこともあります。

## 4. 新たにmodelsディレクトリ直下にファイルを作成し、クラスを定義
app/modelsディレクトリ配下にdonation_address.rbを作成しましょう。

    ＜イメージ図＞ 
    app
     |
     models
       |
       donation_address.rb

作成したファイルにクラスを定義し、DonationAddressクラスにActiveModel::Modelをincludeする。

    # app/models/donation_address.rb
    class DonationAddress
      include ActiveModel::Model
    end

保存したいカラム名を属性値として扱えるようにします
DonationAddressクラス内でattr_accessorを使用し、
donationsテーブルとaddressesテーブルに保存したいカラム名を、すべて指定する。

    # app/models/donation_address.rb
    class DonationAddress
      include ActiveModel::Model
      attr_accessor :postal_code, :prefecture, :city, :house_number, :building_name, :price, :user_id ⇦追記箇所
    end

続いて、バリデーションを同ファイル内に追記する

    class DonationAddress
      include ActiveModel::Model
      attr_accessor :price, :user_id, :postal_code, :prefecture, :city, :house_number, :building_name
    
    　　　＜追記箇所（ここから）＞
      with_options presence: true do
        validates :price, numericality: {only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 1000000, message: 'is invalid'}
        validates :user_id
        validates :postal_code, format: {with: /\A[0-9]{3}-[0-9]{4}\z/, message: "is invalid. Include hyphen(-)"}
      end
      validates :prefecture, numericality: {other_than: 0, message: "can't be blank"}
    　　　　＜追記箇所（ここまで）＞
    end

user_idに対して、presence: trueのバリデーションを新たに追加しています。

donationsテーブルに保存されるuser_idには、本来belongs_to :userのアソシエーションにより、バリデーションが設定されています。
すなわち、Donationモデルがインスタンスを保存する際に紐付くユーザー情報がない場合、モデル内のアソシエーションの記述により保存できません。

しかし、donation_addressクラスにはアソシエーションを定義することはできないため、belongs_toによるバリデーションを行うことができません。
そこで、donation_addressクラスでuser_idに対してバリデーションを新たに設定しました。
（donation.rbとaddress.rbのバリデーションは不要になるため、削除する。）


続いて、Formオブジェクトに、フォームから送られてきた情報をテーブルに保存する処理を記述。

    class DonationAddress
      include ActiveModel::Model
      attr_accessor :price, :user_id, :postal_code, :prefecture, :city, :house_number, :building_name
    
      with_options presence: true do
        validates :price, numericality: {only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 1000000, message: "is invalid"}
        validates :user_id
        validates :postal_code, format: {with: /\A[0-9]{3}-[0-9]{4}\z/, message: "is invalid. Include hyphen(-)"}
      end
      validates :prefecture, numericality: {other_than: 0, message: "can't be blank"}
    
    　　　　＜追記箇所（ここから）＞
      def save
        # 寄付情報を保存し、変数donationに代入する
        donation = Donation.create(price: price, user_id: user_id)
        # 住所を保存する
        # donation_idには、変数donationのidと指定する
        Address.create(postal_code: postal_code, prefecture: prefecture, city: city, house_number: house_number, building_name: building_name, donation_id: donation.id)
      end
      ＜追記箇所（ここまで）＞
    end

## 5. コントローラーでFormオブジェクトのインスタンスを生成するようにする
new, createアクションで、Formオブジェクトのインスタンスを生成します。
理由は2つあります。

- Formオブジェクトのインスタンスをform_withのmodelオプションに指定するため
newアクションで生成するインスタンス変数は、new.html.erb内でも使用できます。
すなわち、newアクションで生成したインスタンスは、form_withのmodelオプションに指定できるということです。
そうすることによって、Formオブジェクトのインスタンスに紐付いたフォームを作成することができます。

- 入力した内容やエラーメッセージをフォームで表示させるため
createアクションで生成したインスタンスと、理由1で説明したform_withのmodelオプションによって実現されます。
createアクションでエラーハンドリングしていた場合、ストロングパラメーターによって値を取得したインスタンスが、renderで表示されたビューのmodelオプションに指定されます。
エラーハンドリングで表示されるフォームは、理由1によってFormオブジェクトのインスタンスに紐付いているため、送信前に入力した内容や、インスタンスに起こっているエラーを表示できます。
さらに、フォームがFormオブジェクトのインスタンスに紐付くことにより、送信されるパラメーターは、donation_addressハッシュを含む二重構造になります。

したがって、requireメソッドの引数に:donation_addressを指定する必要があります。

    # 記載例
    class DonationsController < ApplicationController
      before_action :authenticate_user!, except: :index
    
      def index
      end
    
      def new
        @donation_address = DonationAddress.new
      end
    
      def create
        @donation_address = DonationAddress.new(donation_params)
        if @donation_address.valid?
          @donation_address.save
          redirect_to root_path
        else
          render :new
        end
      end
    
      private
    
      def donation_params
        params.require(:donation_address).permit(:postal_code, :prefecture, :city, :house_number, :building_name, :price).merge(user_id: current_user.id)
      end

## 6.　フォームのmodelオプションをFormオブジェクトのインスタンス変数に変更する
    # 記述例（new.html.erb内）
    <%= form_with model: @donation_address, url: donations_path, local: true do |f| %>

    ※送信先のURL情報にも注意

## (7. エラーメッセージが表示されるようにする）
フォームのmodelオプションにインスタンス変数を指定したことで、バリデーションに失敗したとき、エラーメッセージを表示し、入力した値を持ち越せるようになりました。
インスタンスにエラーがある場合には、エラーメッセージが表示されるようにしましょう。
まずは、メッセージを表示させるための部分テンプレート_error_messages.html.erbを作成する。

    ＜イメージ図＞
    app
    　　|
     views
       |
       donations
         |
         _error_messages.html.erb

作成した部分テンプレートを編集する

    # 記載例
    <% if model.errors.any? %>
      <div class="error-alert">
        <ul>
          <% model.errors.full_messages.each do |message| %>
            <li class="error-message">
              <%= message %>
            </li>
          <% end %>
        </ul>
      </div>
    <% end %>

続いて、新規寄付ページから先ほど作成した部分テンプレートを読み込めるようにする。

    <%= form_with model: @donation_address, url: donations_path, local: true do |f| %>
      <%= render 'error_messages', model: @donation_address %>　　⇦追記箇所
      <h1>住所</h1>
      <div class="field">
       <%= f.label :postal_code, "郵便番号（ハイフンを含む）" %>
       <%= f.text_field :postal_code %>
      </div>
    <%# 省略 %>

## 補足
単体テストコードを作成する場合は、Userモデルと、formオブジェクトであるDonationAddressクラスに分けてテストを書く。

＜注意点＞
DonationAddressクラスのテストを行うときには、直前で生成したUserのインスタンスのidをuser_idに指定しましょう。
本来であれば、FactoryBot内でassociationメソッドを使用することにより、寄付情報に紐付くユーザーのインスタンスを自動的に生成できます。
しかし、DonationAddressクラスはモデルではないため、associationメソッドを使用することができません。
したがって、DonationAddressクラスのインスタンスを生成するときに、紐付くユーザー情報を指定するよう記述します。
    
    # 記述例
    before do
          user = FactoryBot.create(:user)
          @donation_address = FactoryBot.build(:donation_address, user_id: user.id)
    end