複数テーブルに複数レコードをフォーム1つで保存する

### 環境:

ruby 2.7.3

rails 6.1.4.4

# やりたいこと

form_withとfields_forを使って、関連性のない複数のテーブルに、複数のレコードを、ボタン1クリックで保存する。

![スクリーンショット 2022-03-08 16.01.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496677/ff1a9dbd-e992-83c9-daea-4d9f0010871e.png)


猫のコメントはCatテーブル、犬のコメントはDogテーブルに保存する。さらに、それぞれの種類に対するコメントをボタン1つで一括登録しようというのが今回のお題。

### 全体の流れ

1. 入力ページにアクセスすると、コントローラー側でモデルCatとモデルDogをまとめた擬似モデルのインスタンス(=@formとする)が作成される
2. 擬似モデルのinitializeメソッドが走り、モデルCatに対応するattributesのインスタンス、モデルDogに対応するattributesのインスタンスが作成される
3. コントローラー側で、ビューのフォームを表示させるためのインスタンスが作成される
4. フォームからpost送信されたデータがパラメータに格納される
5. モデルCat,Dogに対応するそれぞれのパラメータの中身を保存する

### 1. 入力ページにアクセスすると、コントローラー側でモデルAとモデルBをまとめた擬似モデルのインスタンス(=@formとする)が作成される

コントローラー

```ruby
@form = Form::Collection.new
```

ここでは、`Form::Collection`というクラスのインスタンスを作成している。このクラスは、モデルCatとモデルDogのデータをまとめて保存するための擬似モデル(直接DBと関連しないけれどモデルとして扱えるようにしたクラス)を自作する。

ダブルコロン「::」の意味についてはこちら

[https://qiita.com/hatorijobs/items/87a2bd93f8666d77d711](https://qiita.com/hatorijobs/items/87a2bd93f8666d77d711)

次に、この疑似モデルの中身を少し見ていく。

(collection.rb)

```ruby
class Form::Collection < Form::Base
  attr_accessor :cats, :dogs
```

まず、`Form:Base`というクラスを継承させている。この`Form:Base`の中身は以下のようになっている。

```ruby
class Form::Base
  include ActiveModel::Model
  include ActiveModel::Callbacks
  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks
end
```

つまるところ、このクラスをあたかもモデルのように扱うためにActiveModelをincludeしている。

そして、モデルCatとモデルDogに対応した変数を定義する。

```ruby
attr_accessor :cats, :dogs
```

attr_accessorについてはこちら

[https://qiita.com/k-penguin-sato/items/5b75be386be4c55e3abf](https://qiita.com/k-penguin-sato/items/5b75be386be4c55e3abf)

### 2.擬似モデルのinitializeメソッドが走り、モデルCatに対応するattributesのインスタンス、モデルDogに対応するattributesのインスタンスが作成される

`Form::Collection.new`をすると、まずinitializeメソッドが走る

```ruby
def initialize(attributes = {})
    super attributes
    self.cats = [] unless cats.present?
    self.dogs = [] unless dogs.present?
  end
```

まず、attributesがnilなら入れ物(=空っぽのハッシュ)を用意する。

newしたときに、擬似モデルで設定した変数(cats, dogs)がnilなら、それにも入れ物を用意する。

### 3.コントローラー側で、ビューのフォームを表示させるためのインスタンスが作成される

```ruby
def new
    @form = Form::Collection.new
    cats = Animal.where(animal_type: 'cat')
	dogs = Animal.where(animal_type: 'dog')
    @cat_name = []
	@dog_name = []
    # viewにわたすフォームのインスタンス(猫用)を作成する
    cats.each do |cat|
      @cat_name.push(cat.cat_name)
      comment = Cat.new(cat_comment_id: cat.animal_id)
      @form.cats.push(comment)
    end
    # viewにわたすフォームのインスタンス(犬用)を作成する
    dogs.each do |dog|
    @dog_name.push(dog.dog_name)
    comment = Dog.new(dog_comment_id: dog.animal_id)
    @form.dogs.push(comment)
    end
  end
```

ポイントは以下

```ruby
    comment = Dog.new(dog_comment_id: dog.animal_id)
    @form.dogs.push(comment)
```

これで、@formに格納されている2つの変数に対して、モデルCatとモデルDogのインスタンスが挿入された。インスタンスを複数入れたい場合(=レコードを複数入れたい場合)は.eachや.timesなどでインスタンスを@form.xxx(変数名)に複数個詰め込んでおく。

※モデルに対応したインスタンスを@form.xxxに詰め込んで置かないと、後述するfields_forを使う際に(中身が何もないので)何も表示されなくなってしまう。

### 4.フォームからpost送信されたデータがパラメータに格納される

```ruby

<%= form_with(model: @form, url: { :controller => 'comment', :action => 'create' }, method: :post) do |f| %>
    <%= f.fields_for :cats do |i| %>
			#フォームの内容
    <% end %>
    <%= f.fields_for :dogs do |i| %>
			#フォームの内容
    <% end %>
    <%= submit_tag 'この内容で登録する' %>
  <% end %>

```

form_withのmodelには、@formを渡す。つまり、モデルAとモデルBを両方扱う`Form::Collection`に対応するフォームとしている。

次のf.fields_for には、擬似モデルで設定した変数をそれぞれ渡す。複数あるインスタンスを1個1個ブロック変数iに渡すので、コントローラーで詰め込んだインスタンスの数だけフォームが生成される。

ただ、この状態だと複数個のレコードを送れないので、複数個送れるように`Form::Collection` で設定しておく必要がある。

```ruby
def cats_attributes=(attributes)
   self.cats = attributes.map { |_, v| Cat.new(v) }
  end

  def dogs_attributes=(attributes)
   self.dogs = attributes.map { |_, v| Dog.new(v) }
  end
```

(処理の中身)

まず、xxx_attributesという名前で送られてきたパラメータを一旦引数で指定したattributesという変数に代入する。

@form.xxx に対して、attributesの個数だけモデルAのインスタンスとして.mapで中身を代入する。

mapメソッド

[https://www.sejuku.net/blog/58958](https://www.sejuku.net/blog/58958)

補足: |_, v|のアンダースコアは、「使わないけど一旦代入しておく変数」です。

[https://qiita.com/jnchito/items/3cce0c057f54afa29d0a](https://qiita.com/jnchito/items/3cce0c057f54afa29d0a)

フォームを送信すると、以下のようなパラメータが送られてくる。

```ruby
Parameters: {"authenticity_token"=>"[FILTERED]", 

"form_collection"=>

	{"cats_attributes"=>
		{"0"=>{"cat_comment_id"=>"それぞれの猫のID","comment"=>"入れたコメント"},
		{"1"=>{"cat_comment_id"=>"それぞれの猫のID","comment"=>"入れたコメント"},
		{"2"=>{"cat_comment_id"=>"それぞれの猫のID","comment"=>"入れたコメント"}},

	{"dogs_attributes"=>
		{"0"=>{"dog_comment_id"=>"それぞれの犬のID","comment"=>"入れたコメント"},
		{"1"=>{"dog_comment_id"=>"それぞれの犬のID","comment"=>"入れたコメント"},
		{"2"=>{"dog_comment_id"=>"それぞれの犬のID","comment"=>"入れたコメント"}},
"commit"=>"この内容で登録する"}
```

ポイントは、「xxx_attributes」というキー。このキーは、「(疑似モデルで設定した変数名)_attributes」という形になっている。

### 5.モデルCat,Dogに対応するそれぞれのパラメータの中身を保存する

パラメータの中に、複数のテーブルに対応した複数のレコードが詰め込まれているので、あとはそれをバラして個別に保存していく。

コントローラー

```ruby
def create
    @form = Form::Collection.new(comment_collection_params)
		
# それぞれのレコードに対して必要な情報を一括で入れ込む
    @form.cats.each do |cat|
      cat.animal_type = 'cat'
    end
    @form.dogs.each do |dog|
      dog.animal_type = 'dog'
    end
end

if @form.cats.map(&:save) and @form.dogs.map(&:save)
      redirect_to action: :complete
    end

private

def comment_collection_params
    params
      .require(:form_collection)
      .permit(cats_attributes: %i[許可するカラムの名前],
              dogs_attributes: %i[許可するカラムの名前])
  end
```

まず、`Form::Collection`のインスタンスを作成し、ストロングパラメータ化したcomment_collection_paramsを渡している。

@formの中身はこんな感じ

```ruby
↓@form.catsの中身↓
[
#<Cat:0x00000001086a19f0
	カラム: データ
	カラム: データ
	カラム: データ>,
#<Cat:0x00000001086a14c8
	カラム: データ
	カラム: データ
	カラム: データ>,
 #<Cat:0x00000001086a1018
	カラム: データ
	カラム: データ
	カラム: データ>,
]
```

ストロングパラメータについてはこちら

[https://qiita.com/ozackiee/items/f100fd51f4839b3fdca8](https://qiita.com/ozackiee/items/f100fd51f4839b3fdca8)

一括して同じデータを入れたいカラムについては、それぞれeachでバラして代入していく形。

さらにその後、

```ruby
@form.cats.map(&:save) and @form.dogs.map(&:save)
```

それぞれのモデルに対して複数個あるレコードを保存する。

(&:save)はmapメソッドの省略形

[https://pikawaka.com/ruby/map](https://pikawaka.com/ruby/map)

### 参考

1つのフォームで複数のテーブルを扱う

[https://www.petitmonte.com/ruby/multiple_models.html](https://www.petitmonte.com/ruby/multiple_models.html)

複数レコードを一括で保存する

[https://ryucoding.com/programming/rails-form-bulk-create](https://ryucoding.com/programming/rails-form-bulk-create)