やりたいこと
1対多の関係を持つモデルに対し、同時にレコードを登録する。
検索した際によく出てきたaccepts_nested_attributes_forは使わず、一括登録をするためのモデルを別で用意して実装。
前提条件
下記のように、一人のcustomerが多くのreservationsを持つ、1対多の関係。
has_many :reservations
belongs_to :customer
考え方
1. customerとreservationを一緒に扱うためのモデル(customer_form)を作成。
2. 作成したモデル内で、架空のテーブルを作成し、2つ同時に登録するためのsaveメソッドを定義する。※実際はテーブルは存在しません。イメージです。
3. customerコントローラでnewアクション、createアクションを定義。その際、②で作成したモデルのsaveメソッドを使う。
4. viewを作成する。
5. レコードを登録した後に、作成した情報を表示させたページにリンクさせる。(今回は編集ページに飛ばしてます)そのために必要なidの情報を、モデルからコントローラーへ受け渡すためのメソッドを作成する。
6. ⑤で作成したメソッドからidを取得し、遷移先をコントローラに記述する。
実装
1.customerとreservationを一緒に扱うためのモデル(customer_form)を作成。
一括登録するためのmodelを作成
$ rails g model CustomerForm
テーブルは必要ないのでmigrationファイルは削除。
2.作成したモデル内で、架空のテーブルを作成し、2つ同時に登録するためのsaveメソッドを定義する。※実際はテーブルは存在しません。イメージです。
class CustomerForm
include ActiveModel::Model
attr_accessor :name_kana, :name, :postal_code, :main_address, :sub_address, :phone, :created_at, :updated_at, :check_in, :check_out, :price, :status_id
validates :name_kana, format: { with: /\A[ -~。-゚]*\Z/ }, presence: true
validates :check_in, :check_out, presence: true
def save
# return false in invalid? バリーデションの追加ができる
@customer = Customer.new(name_kana: name_kana, name: name, postal_code: postal_code, main_address: main_address, sub_address: sub_address, phone: phone, created_at: created_at, updated_at: updated_at)
@customer.reservations.new(check_in: check_in, check_out: check_out, price: price, status_id: status_id)
@customer.save
end
end
上から順に解説。間違ってたらご指摘ください。
include ActiveModel::Modelを記載することで、ActiveModelの機能が使えるようになる。
その以下の行で、架空のテーブルを作成するようなイメージ。attr_accessorの後に、このモデルのカラムを定義する。今回は、customerモデルのカラムと、reservationモデルのカラムの値を受け取りたいので、その2つのモデルが持つカラムを記述。
おそらくその直後でいつも通りカラムに対しバリデーションをつけられるはず。ちゃんと弾かれるかはまだ確認してない。
customerコントローラで動かすためのsaveメソッドを作成。
ここでcustomerのインスタンスとreservationのインスタンスを同時生成し、保存している。
3.customerコントローラでnewアクション、createアクションを定義。その際、②で作成したモデルのsaveメソッドを使う。
def new
# @statuses = Status.all
@customer_form = CustomerForm.new
end
def create
@customer_form = CustomerForm.new(customer_reservation_params)
if @customer_form.save
# redirect_to "/customers/#{@customer_form.customer_id}}/reservations/#{@customer_form.reservation_id}/edit"
else
redirect_to new_customer_path
end
private
def customer_reservation_params
params.require(:customer_form).permit(:name_kana, :name, :postal_code, :main_address, :sub_address, :phone, :created_at, :updated_at,
:check_in, :check_out, :price, :status_id)
end
今関係ないものはややこしいのでコメントアウトしています。
viewのform_withのモデルの指定に使うので、newアクションでCustomerFromのインスタンスを変数に格納。
createアクションでcustomer_formのインスタンスを作成。
その際にストロングパラメーターでcustomer_formモデルのカラムを許可する。(実際は、customerモデルとreservationモデルの2つ分のカラムの値を許可することになります)
require先は、作成したcustomer_formモデルにすることに注意。
@customer_form.save部分で、②で作成したcustomer_formモデルのsaveメソッドが動くので、customerのインスタンスとreservationのインスタンスを同時生成し、保存することが可能になりました。
4.viewを作成する
<%= form_with(model: @customer_form, url: customers_path) do |f| %>
<%= f.collection_select :status_id, @statuses, :id, :name%> #reservationsテーブルのカラム
到着日<%= f.date_field :check_in %> #reservationsテーブルのカラム
出発日<%= f.date_field :check_out %> #reservationsテーブルのカラム
フリガナ<%= f.text_field :name_kana, placeholder: '半角のみ' %> #customersテーブルのカラム
名前<%= f.text_field :name %> #customersテーブルのカラム
住所 〒<%= f.text_field :postal_code %> #customersテーブルのカラム
<%= submit_tag '登録' %>
<% end %>
form_withのモデルの指定に、先ほど作成したcustomer_formを指定することで、何も意識することなく通常通りフォームの作成ができます。
本来、customerモデルが持っているカラムも、reservationモデルが持っているカラムも記述の仕方に変わりはないので、実際の記述よりかなり省略してます。
5.レコードを登録した後に、作成した情報を表示させたページにリンクさせる。(今回は編集ページに飛ばしてます)そのために必要なidの情報を、モデルからコントローラーへ受け渡すためのメソッドを作成する
edit_customer_reservation GET /customers/:customer_id/reservations/:id/edit(.:format) reservations#edit
登録ボタンを押した後に、上記のパスに飛ばしたい。
そのためには、:customer_idとreservations/:idを取得しなければいけない。
いろいろ試したのですが、どうもcustomers_controllerでは保存した瞬間のレコードのidが取得できなかったので、customer_formモデルから引っ張ってきました。
記事を書いた翌朝に気づいたのですが、レコードがsaveされた瞬間にidが発行されるため、実際にレコードを保存しているsave自体はモデルで行なっているため、モデルでしか取得できなかったんだと思います。
# def save
# return false in invalid?
# @customer = Customer.new(name_kana: name_kana, name: name, postal_code: postal_code, main_address: main_address, sub_address: sub_address, phone: phone, created_at: created_at, updated_at: updated_at)
# @customer.reservations.new(check_in: check_in, check_out: check_out, price: price, status_id: status_id)
# @customer.save
# end
def customer_id
return @customer.id
end
def reservation_id
return @customer.reservations.ids[0]
end
デバッグを使いターミナルで確認したところ、@customer.idで保存したばかりのidが取得できたので、それを
customersコントローラで使うためにメソッドにしています。
同じく、reservationのidも@customer.reservations.idsで取得できたため、メソッドで返します。ただ、こちらに関してはidsの配列に入っているため[0]を指定して、値を取り出しています。これがないと[id]になってしまい、ルーティングエラーになるため注意。
6.⑤で作成したメソッドからidを取得し、遷移先をコントローラに記述する。
def create
@customer_form = CustomerForm.new(customer_reservation_params)
if @customer_form.save
redirect_to "/customers/#{@customer_form.customer_id}}/reservations/#{@customer_form.reservation_id}/edit"
else
redirect_to new_customer_path
end
end
@customer_form.customer_id、@customer_form.reservation_idの記述により、先ほど作成したcustomer_formモデルのそれぞれのメソッドを呼び出し、リンクさせることに成功!
備考
どこかの質問ページの回答を参考(というかほぼ丸パクリ)にしてなんとか実装できました。
自分が忘れないために記事にしてみました。実際のコードを消したりしながら作ったので間違いがあったらご指摘ください。
また、改善策などあれば教えてください。
説明が下手なのでわかりにくいと思いますが、誰かの参考になれば幸いです。