フォームオブジェクトで複数のモデルを操作する[脱 accepts_nested_attributes_for]

かなり丁寧に書いたのでボリューム多めです。
分かっている部分はガンガン読み飛ばしてください:thumbsup:

# やりたいこと
・`accepts_nested_attributes_for`を使わず複数のモデルを操作したい。
・データの新規作成だけでなく、更新もしたい。

# 事前準備
### accepts_nested_attributes_forとは？
関連付けられたモデルのレコードを一度に更新できるメソッドです。

例）メッセージに複数の画像をつけて送信する場合
![er_for_message.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617574/828e917d-a1c7-670b-bace-c7d2ce74e2e2.png)

上記の場合だと、メッセージとそれに紐づいた画像を保存できます。便利ですね。
ただ、**実はこのメソッドはあまり評判が良くありません**:joy:[(詳しくはこちら)](https://github.com/rails/rails/pull/26976#discussion_r87855694)

そこで、代替案のフォームオブジェクトを紹介していきます。

### フォームオブジェクトとは？
モデルから切り離され、フォームの処理用に独立したクラスです。

これを使うと**`accepts_nested_attributes_for`なしで複数のレコードを更新できます。**
その他メリットや原理について詳しく知りたい方は[こちらの記事](https://qiita.com/9ro/items/10e3676741e09ffb98bb)を。説明がめちゃくちゃ分かりやすいです:pray:

# 本記事の目標
ユーザー（親）がメッセージ（子）に画像（孫）を紐づけて送信できる機能を実装します。

親、子、孫モデルは皆さんの状況に合わせて変えてください。

# 割愛した部分
・ルーティング設定
・画像投稿に必要なcarrierwaveの設定
・各モデルのテーブル作成

# 実装例
それでは見ていきましょう！

## モデル

```ruby:user.rb
class User < ApplicationRecord
  has_many :messages
end
```

```ruby:message.rb
class Message < ApplicationRecord
  has_many :pictures
  belongs_to :user
  # ここに書いていたフォームのバリデーションは、フォームオブジェクトに移ります。
end
```

```ruby:picture.rb
class Picture < ApplicationRecord
  belongs_to :message
  # 画像投稿しない方は、以下の記述は不要です。
  mount_uploader :picture, PictureUploader
end
```

バリデーションに関する記述がなくなりました。
スッキリして読みやすいですね:relaxed:

## コントローラ
ここで注目して欲しいのは、下記の2点だけです。

・`assigns_attributes`でパラメータから渡された値をもとに`@message`の情報を更新。
・`MessageForm`は、後ほど解説するフォームオブジェクト用のクラス。

```ruby:messages_controller.rb
  # 新規作成画面
  def new
    @message = MessageForm.new
  end

  # 新規作成
  def create
    @message = MessageForm.new
    @message.assign_attributes(message_form_params)
    if @message.save
      # 成功・失敗の処理
    end
  end

  # 編集画面
  def edit
    # params[:id]の部分は、編集するオブジェクトを特定できる値を入れてください
    @message = MessageForm.new(message = Message.find(params[:id]))
  end

  # 編集
  def update
    @message = MessageForm.new(message = Message.find(params[:id]))
    @message.assign_attributes(message_form_params)
    if @message.save
      # 成功・失敗の処理
    end
  end

  private
    # ストロングパラメータ
    def message_form_params
      params.require(:message_form).permit(:body, pictures_attributes: [:picture]).merge(user_id: current_user)
    end
```

ストロングパラメータの`pictures_attributes`については、次に説明していきます。

## ビュー
メッセージの新規作成画面です。

```ruby:new.html.erb
<%= form_with model: @message, url: メッセージ作成用のpath, local: true do |f| %>

  <%= f.text_area :body %>

  <%= f.fields_for :pictures do |picture| %>
    <%= picture.file_field :picture, multiple: "multiple", name: "message_form[pictures_attributes][][picture]" %>
  <% end %>

  <%= f.submit "送信" %>

<% end %>

```

パラメータを中心に解説します。

まず最初に、テキストエリアに「OK」と入力して送信したとします。
このときのパラメータは下記のようになります。（ターミナルで確認してみてください。）

```
Parameters: {"authenticity_token"=>"略", "message_form"=>{"body"=>"OK"}, "button"=>""}
```

bodyカラムに入力した値が入っていますね。

ここで、`"message_form"`となっているのは`@message`が`MessageForm`クラスから生成しているからです。（自動でこうなります。）

次に、これに画像情報を追加するなら、下記のような形にしたいですよね。

```
Parameters: {"authenticity_token"=>"略", "message_form"=>{画像情報, "body"=>"OK"}, "button"=>""}
```

そこで、`file_field`のname属性を使ってみます。

```ruby
name: "message_form[pictures_attributes][][picture]"
```

こう書くと、画像を2枚投稿した場合のパラメータは下記になります。
（画像情報は長いので省略しています。）

```
 Parameters: {"authenticity_token"=>"略", "message_form"=>{"pictures_attributes"=>[{"picture"=>1枚目の画像情報}, {"picture"=>2枚目の画像情報}], "body"=>"OK"}, "button"=>""}
```

`"message_form"`下に画像情報が入ったのが分かると思います。

また、`"pictures_attributes"=>[{"picture"=>`がコントローラのストロングパラメータに対応しています。

ひとまず、これでコントローラに値を渡せそうですね:relaxed:

**ちなみに、編集画面の場合は注意が必要です。**
理由は[こちらの記事](https://qiita.com/keisuke_x_japan/items/ac0e93fd6592baa27815)に書いていますので、時間があれば参考にしてください。

## フォームオブジェクト用クラス
ここが一番ややこしい部分です。
4つに分けて解説しますので、気楽に読んでください:ok_hand:

```ruby:message_form.rb
class MessageForm

  # part-1
  include ActiveModel::Model
  include Virtus.model
  extend CarrierWave::Mount

  validates :body,   presence: true

  attribute :body, String
  attribute :user_id, Integer

  mount_uploader :picture, PictureUploader
  attr_accessor :pictures

  # part-2
  def initialize(message = Message.new)
    @message = message
    self.attributes = @message.attributes if @message.persisted?
  end

  # part-3
  def assign_attributes(params = {})
    @params = params
    pictures_attributes = params[:pictures_attributes]
    @pictures ||= []
    pictures_attributes&.map do |pictures_attribute|
      picture = Picture.new(pictures_attribute)
      @pictures.push(picture)
    end
    @params.delete(:pictures_attributes)
    @message.assign_attributes(@params) if @message.persisted?
    super(@params)
  end

  # part-4
  def save
    return false if invalid?
    if @message.persisted?
      @message.pictures = pictures if pictures.present?
      @message.save!
    else
      message = Message.new(user_id: user_id,
                            body: body)
      message.pictures = pictures if pictures.present?
      message.save!
    end
  end

end

```

### part-1（モジュールの導入）

```ruby:
  include ActiveModel::Model
  include Virtus.model
  extend CarrierWave::Mount

  validates :body, presence: true

  attribute :body, String
  attribute :user_id, Integer

  mount_uploader :picture, PictureUploader
  attr_accessor :pictures
```

ここはインクルードしたモジュールなどをまとめています。

・`ActiveModel::Model`は、`MessageForm`オブジェクトを保存する際にバリデーションを使えるようにしています。`validates`の部分。`ActiveRecord`を継承していなくてもバリデーションできるのが便利。

・`Virtus.model`は、このクラスの属性名と型を定義しています。`attribute`の部分。
何をパラメータとして受け取るのか明示しています。ちなみに、gemの[virtus](https://github.com/solnic/virtus)をインストールしています。

・`CarrierWave::Mount`は画像のアップロード用です。`mount_uploader`の部分。

`attr_accessor`はメッセージに画像情報をセットするためのものです。（part-4で使います。）

### part-2（オブジェクトの初期化）

オブジェクトにプロパティを持たせます。

```ruby:
  def initialize(message = Message.new)
    @message = message
    self.attributes = @message.attributes if @message.persisted?
  end
```

ここは`initialize`の引数がポイント。
コントローラの`new`メソッドに引数がある場合、それを受け取っています。

```ruby:
  def edit
    @message = MessageForm.new(message = Message.find(params[:id]))
  end
```

つまり、`new`でオブジェクトを生成した際に、すでに作成したメッセージの情報があれば、それをもとにフォームオブジェクトの属性を書き換えています。
(`self.attributes`の部分)

### part-3（オブジェクトの更新）
そろそろ疲れてきた方はすみません。もう少しの辛抱です。。。

```ruby:
  def assign_attributes(params = {})
    @params = params
    pictures_attributes = params[:pictures_attributes]
    # picturesと書けば、画像情報が呼び出せる
    @pictures ||= []
    pictures_attributes&.map do |pictures_attribute|
      picture = Picture.new(pictures_attribute)
      @pictures.push(picture)
    end
    # パラメータから画像情報を削除
    @params.delete(:pictures_attributes)
    # すでに作成したメッセージの情報を更新
    @message.assign_attributes(@params) if @message.persisted?
    # フォームオブジェクトの情報を更新
    super(@params)
  end
```

ここでは`assign_attributes`で`@message`にパラメータの値をセットしています。

`assign_attributes`メソッドはもともとデータ更新用のメソッドなのですが、

・フォームオブジェクトが持つ編集用の情報を、すでに作成したメッセージに反映させたい
・`pictures`と書けば画像情報を取得orセットできるようにしたい(part-2の`attr_accessor`)

という理由から少し改造しています:sunglasses:

### part-4（変換作業）
いよいよラスト！`Message`モデルへの変換作業です。

```ruby:
  def save
    # 保存前にバリデーションをかける
    return false if invalid?
    # 編集する場合の処理
    if @message.persisted?
      @message.pictures = pictures if pictures.present?
      @message.save!
    else
    # 新規作成の場合の処理
      message = Message.new(user_id: user_id,
                            body: body)
      message.pictures = pictures if pictures.present?
      message.save!
    end
  end
```

編集の場合は、普通の保存と変わりないですね。
このときの`@message`は、part-2の`message`と同じです。

大事なのは、下記の`Message`オブジェクトへの変換。

```ruby:
      message = Message.new(user_id: user_id,
                            body: body)
```
フォームオブジェクトが持ってきた値をカラムに渡しています。
これで`MessageForm`から`Message`オブジェクトへの変換が完了しました。

```ruby:
      message.pictures = pictures if pictures.present?
```
最後に、画像情報をメッセージにセットして終了です。
お疲れ様でした:clap:

# 環境
ruby: 2.7.1
rails: 6.0.3.3