##はじめに
一画面で一つのモデルを一括登録するのにかなり時間がかかったので備忘録として残します。
本記事の一番下に完成形のコードがありますので、コードだけ見たい方はそちらをご覧ください。
##前提条件
ユーザーモデル
ユーザーモデル |
---|
name |
address |
age |
class User < ApplicationRecord
# 入力必須
validates :name, presence: true
end
##一括登録する手順
- 複数のユーザーを格納できるコレクションモデルを作成
- 一画面で複数のデータを登録できるフォームを作成
- コントローラーでフォームのデータを取得
- 複数登録用ののsaveメソッドを作成
###1.複数のユーザーを格納できるコレクションモデルを作成
class UserCollection
include ActiveModel::Conversion
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::AttributeMethods
include ActiveModel::Validations
USER_NUM = 5 # 同時にユーザーを作成する数
attr_accessor :collection # ここに作成したユーザーモデルが格納される
# 初期化メソッド
def initialize(attributes = [])
if attributes.present?
self.collection = attributes.map do |value|
User.new(
name: value['name'],
address: value['address'],
age: value['age']
)
end
else
self.collection = USER_NUM.times.map{ User.new }
end
end
# レコードが存在するか確認するメソッド
def persisted?
false
end
end
最初にユーザーコレクションクラスをモデル化します。
下記の記事が分かりやすかったのでこちらをご覧ください。
次に初期化時の処理です。
initializeメソッドでは引数があった場合、その引数を使って複数のユーザーモデルを作成します。
引数がなかった場合はUSER_NUMの個数分ユーザーモデルを作成します。
作成したユーザーモデルはユーザーコレクションのcollectionに配列として格納されます。
これで以下のプログラムを記述するとユーザーモデルが5つ生成されます。
@users = UserCollection.new
###2. 一画面で複数のデータを登録できるフォームを作成
def new
@users = UserCollection.new
end
<h1>New User</h1>
<%= form_with model: @users, url: users_path, local: true do |form| %>
<% @users.collection.each do |user|%>
<%= fields_for 'users[]', user do |field| %>
<div class="field">
<%= field.label :name %>
<%= field.text_field :name %>
</div>
<div class="field">
<%= field.label :address %>
<%= field.text_field :address %>
</div>
<div class="field">
<%= field.label :age %>
<%= field.number_field :age %>
</div>
</br>
<% end %>
<% end %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
<%= link_to 'Back', users_path %>
form_withとfields_forを使って複数登録できるフォームを作成します。
ここではfields_forが複数のデータを登録する大きなポイントとなってきます。
form_withは1つのデータしか登録できず、複数のデータを一気に登録することができません。
その問題点を解消する為に使うのがfields_forです。
fields_forは複数のモデルを登録することができます。
今回のパターンではユーザーコレクションの配列をループして、配列の要素数分のfields_forを作成しています。
次にfields_forの引数についての解説です。
以下のようにすることで配列形式でユーザーモデルのデータを送信することができます。
# 抜粋
<%= fields_for 'users[]', user do |field| %>
# 実際に送信したパラメーター
"users"=>[
{"name"=>"w", "address"=>"w", "age"=>"1"},
{"name"=>"q", "address"=>"d", "age"=>"1"},
{"name"=>"e", "address"=>"f", "age"=>"1"},
{"name"=>"r", "address"=>"g", "age"=>"1"},
{"name"=>"f", "address"=>"h", "age"=>"1"}
]
###3. コントローラーでフォームのデータを取得
「2.一画面で複数のデータを登録できるフォームを作成」で送信したユーザーモデルを取得します
...
def create
@users = UserCollection.new(users_params)
if @users.save
redirect_to users_url
else
render :new
end
end
private
def users_params
params.require(:users)
end
# 実際に取得できるパラメーター
[
{"name"=>"w", "address"=>"w", "age"=>"1"},
{"name"=>"q", "address"=>"d", "age"=>"1"},
{"name"=>"e", "address"=>"f", "age"=>"1"},
{"name"=>"r", "address"=>"g", "age"=>"1"},
{"name"=>"f", "address"=>"h", "age"=>"1"}
]
UserCollection.new(users_params)を実行することでUserCollectionにフォームで入力した値が格納されます。
###4. 複数登録用ののsaveメソッドを作成
attr_accessor :collection
# コレクションをDBに保存するメソッド
def save
is_success = true
ActiveRecord::Base.transaction do
collection.each do |result|
# バリデーションを全てかけたいからsave!ではなくsaveを使用
is_success = false unless result.save
end
# バリデーションエラーがあった時は例外を発生させてロールバックさせる
raise ActiveRecord::RecordInvalid unless is_success
end
rescue
p 'エラー'
ensure
return is_success
end
最後にsaveメソッドを作成します。
処理の流れですが、collectionにはフォームで入力した複数のユーザーモデルが配列で格納されています。
その配列をループで回し、ひとつずつ保存していきます。
全てのユーザーモデルをバリデーションチェックしたい為、saveメソッドを使用しています。
もし1つでもバリデーションエラーが発生した場合は、例外を発生させて保存したユーザーモデルをロールバックで戻します。
※transactionは例外しかキャッチしてくれません!
is_successは保存が成功したかを表しています。
最後の処理でis_successを返却し、下記のような処理の分岐に使用しています。
...
def create
@users = UserCollection.new(users_params)
if @users.save
redirect_to users_url
else
render :new
end
end
これでsaveが成功していれば一括登録の完了です!
##まとめ
ここまでご覧いただきありがとうございました。
間違い等あれば指摘いただけると大大大感謝です。
##ソースまとめ
####モデル
ユーザーコレクションモデル
# ユーザーのコレクションモデル
class UserCollection
include ActiveModel::Conversion
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::AttributeMethods
include ActiveModel::Validations
USER_NUM = 5 # 同時にユーザーを作成する数
attr_accessor :collection
# 初期化メソッド
def initialize(attributes = [])
if attributes.present?
self.collection = attributes.map do |value|
User.new(
name: value['name'],
address: value['address'],
age: value['age']
)
end
else
self.collection = USER_NUM.times.map{ User.new }
end
end
# レコードが存在するか確認するメソッド
def persisted?
false
end
# コレクションをDBに保存するメソッド
def save
is_success = true
ActiveRecord::Base.transaction do
collection.each do |result|
# バリデーションを全てかけたいからsave!ではなくsaveを使用
is_success = false unless result.save
end
# バリデーションエラーがあった時は例外を発生させてロールバックさせる
raise ActiveRecord::RecordInvalid unless is_success
end
rescue
p 'エラー'
ensure
return is_success
end
end
####コントローラー
def new
@users = UserCollection.new
end
def create
@users = UserCollection.new(users_params)
if @users.save
redirect_to users_url
else
render :new
end
end
private
def users_params
params.require(:users)
end
####ビュー
<h1>New User</h1>
<%= form_with model: @users, url: users_path, local: true do |form| %>
<% @users.collection.each do |user|%>
<%= fields_for 'users[]', user do |field| %>
<div class="field">
<%= field.label :name %>
<%= field.text_field :name %>
</div>
<div class="field">
<%= field.label :address %>
<%= field.text_field :address %>
</div>
<div class="field">
<%= field.label :age %>
<%= field.number_field :age %>
</div>
</br>
<% end %>
<% end %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
<%= link_to 'Back', users_path %>