1
1

More than 3 years have passed since last update.

1対多のモデル間のテーブルに一つのformで一括登録する

Last updated at Posted at 2019-10-31

やりたいこと

1対多の関係を持つモデルに対し、同時にレコードを登録する。
検索した際によく出てきたaccepts_nested_attributes_forは使わず、一括登録をするためのモデルを別で用意して実装。

前提条件

下記のように、一人のcustomerが多くのreservationsを持つ、1対多の関係。

models/customer.rb
has_many :reservations
models/reservation.rb
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メソッドを定義する。※実際はテーブルは存在しません。イメージです。

models/customer_form.rb
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メソッドを使う。

controllers/customers_controller.rb
  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を作成する

customers/new.html.erb
<%= 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自体はモデルで行なっているため、モデルでしか取得できなかったんだと思います。

models/customer_form.rb
#  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を取得し、遷移先をコントローラに記述する。

controllers/customers_contrller.rb
  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モデルのそれぞれのメソッドを呼び出し、リンクさせることに成功!

備考

どこかの質問ページの回答を参考(というかほぼ丸パクリ)にしてなんとか実装できました。
自分が忘れないために記事にしてみました。実際のコードを消したりしながら作ったので間違いがあったらご指摘ください。
また、改善策などあれば教えてください。
説明が下手なのでわかりにくいと思いますが、誰かの参考になれば幸いです。

1
1
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
1
1