レコードの一括登録をしたかったのですが、form_withにモデルを紐付けるので、複数のレコードを一気に保存する方法が難しく時間かかりました。
備忘録として記事にしてみます。
Contents
やりたいこと
# Memoテーブル
# id : integer
# title : string
class Memo < ApplicationRecord
validates :title, presence: true
end
このようなMemoモデルがあって、それのnewアクションを作るとき、普通の実装では以下になると思います。
それにちょっと手を加え、一つのフォームで入力欄を複数設け一括で登録したいです。
方針
一括登録するようにモデルを作って、そのなかで独自saveメソッドを作成する。独自saveメソッド内で元のモデルのインスタンスを複数作成する
コントローラーの実装
コントローラーのnewアクションではmemoモデルではなく、それを管理するMemoCollectionモデルを生成します。
MemoCollectionモデルは後述しますが、models/form/memo_collection.rb
に作成する予定です。
class MemosController < ApplicationController
def index
@memos = Memo.all
end
def new
@form = Form::MemoCollection.new
end
def create
@form = Form::MemoCollection.new(memo_collection_params)
if @form.save
redirect_to new_memo_path
else
render :new
end
end
private
def memo_collection_params
params
.require(:form_memo_collection)
.permit(memos_attributes: :title)
end
end
ストロングパラメータにはmemos_attributesを受け取るようにします。
これはViewでfields_forを使用していく予定のためです。
fields_forについてはこちら
ビューの実装
viewではfields_forを使いましょう。
= form_with( model: @form ,url: memos_path, method: :post) do |f|
= f.fields_for :memos do |i|
= i.text_field :title
= f.submit "送信"
f.fields_for :memosとしているので、@formであるForm::MemoCollectionモデルには、memosという属性を定義している必要が出てきます。次のモデルのところで実装予定です。
モデルの実装
Memo.rbは普通の実装です。
class Memo < ApplicationRecord
validates :title, presence: true
end
追加で、Form::Baseを継承したForm::MemoCollectionのようなモデルを作成していきましょう。
models/form/memo_collection.rbに作成するのが良いと思います。
MemoCollectionモデルは、attr_accessor :memos
として、Form::MemoCollection.memosが使えるようにしておきましょう。
Form::MemoCollection.new(…)とするために、initializeメソッドを、Form::MemoCollection.saveとするために、saveメソッドを作成しましょう。
自作saveメソッド内で、ループしMemoモデルのインスタンスを送られてきたかずループしてsaveします。
class Form::MemoCollection < Form::Base
FORM_COUNT = 3
attr_accessor :memos
def initialize(attributes = {})
super attributes
self.memos = FORM_COUNT.times.map { Memo.new() } unless self.memos.present?
end
# 上でsuper attributesとしているので必要
def memos_attributes=(attributes)
self.memos = attributes.map { |_, v| Memo.new(v) }
end
def save
# 実際にやりたいことはこれだけ
# self.memos.map(&:save!)
# 複数件全て保存できた場合のみ実行したいので、transactionを使用する
Memo.transaction do
self.memos.map(&:save!)
end
return true
rescue => e
return false
end
end
Form::MemoCollectionの、initializeメソッドでsuperを使いたいので、class Form::MemoCollection < Form::Base
のように継承する必要があります。
なので、models/form/base.rbを作成しましょう!
class Form::Base
include ActiveModel::Model
include ActiveModel::Callbacks
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
end
参考資料
https://qiita.com/Ryoga_aoym/items/91a3940cfa4de268fca4
https://rails.densan-labs.net/form/bulk_registration_form.html
初学者ですが、コメント失礼します。
models/form/memos_collection.rbでの下記のコードはどのような意味なのでしょうか。
コントローラーのprivate以下のメソッドで呼び出していると思うのですが、何をしようとしているのか疑問に思いました。だいぶざっくりした質問で恐縮ですが、ご返信頂ければ幸いです。
def memos_attributes=(attributes)
self.memos = attributes.map { |_, v| Memo.new(v) }
end
また、attribute = {}としている意味など教えて頂ければ幸いです。
コメントありがとうございます!
最近はスパムコメントしかなかったので、めっちゃ嬉しいです。
今回は、
def memos_attributes=(attributes)
self.memos = attributes.map { |_, v| Memo.new(v) }
end
の意味ということですね!
結論からお伝えしますと、saveをする際にForm::Baseクラスのセッター(xxxx_attributes=)が呼ばれるのを上書きしている部分でして、viewでfields_forを使っているため必要になります。
viewでfields_forを利用すると2つの条件が必要となります。
1. fields_for の第一引数に渡した変数名の変数にアクセスできること
2. 指定した変数が xxxx_attributes= (xxx は変数名)という形式で更新できること
1.は今回であれば、:memosですね!(なので、models/form/memos_collection.rbでは、attr_accessorが必要になります。)
2.はfields_forを使用するとparamsとして、xxxx_attributesというキーで送られます。
Form::Baseクラスには、セッター(xxxx_attributes)があり、saveメソッドを使用するとセッターが呼ばれます。
今回やりたいことは、fields_forで送られてきた複数のデータを
self.memos = attributes.map { |_, v| Memo.new(v) }
のようにしてMemo.new(v)することです。そのため、セッター(xxxx_attributes)を上書きしました。
また、attribute = {}というのは、以下の部分ですかね?
def initialize(attributes = {})
initializeメソッドは、
Form::MemoCollection.new(memo_collection_params)
をした際に呼びだされますが、
memo_collection_paramsがnilだった場合困るので、{}として初期化しています。
memo_collection_paramsに値が入っていた場合は、 = {}は無視されます。
回答になってますでしょうか?
ややこしい実装になっていて申し訳ないです-_-
不明点あれば追加で連絡いただければ大変ありがたいです!
[…] 【Rails 6】form_with用いて一括登録する […]
[…] 【Rails 6】form_with用いて一括登録する | RyuCoding […]