5
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-08

環境:

ruby 2.7.3

rails 6.1.4.4

やりたいこと

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

スクリーンショット 2022-03-08 16.01.22.png

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

全体の流れ

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

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

コントローラー

@form = Form::Collection.new

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

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

https://qiita.com/hatorijobs/items/87a2bd93f8666d77d711

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

(collection.rb)

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

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

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

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

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

attr_accessor :cats, :dogs

attr_accessorについてはこちら

https://qiita.com/k-penguin-sato/items/5b75be386be4c55e3abf

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

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

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

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

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

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

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

ポイントは以下

    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送信されたデータがパラメータに格納される


<%= 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 で設定しておく必要がある。

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

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

https://qiita.com/jnchito/items/3cce0c057f54afa29d0a

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

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に対応するそれぞれのパラメータの中身を保存する

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

コントローラー

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の中身はこんな感じ

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

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

https://qiita.com/ozackiee/items/f100fd51f4839b3fdca8

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

さらにその後、

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

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

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

https://pikawaka.com/ruby/map

参考

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

https://www.petitmonte.com/ruby/multiple_models.html

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

https://ryucoding.com/programming/rails-form-bulk-create

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3