前提
あるショップのデータを作成するときに、そのショップへのコメントも一気に作成したいと思います。前提となる環境は以下の通りです。
- Rails 5.2.4.2
- rspec-rails 4.0.1
テーブルについて
テーブルの構造はこんな感じです。
親テーブル(shops)
shops |
---|
id |
name |
子テーブル(comments)
comments |
---|
id |
shop_id |
content |
モデル
親モデル、子モデルのmodelファイルはそれぞれこのようになっています。
親モデル(Shop)
class Shop < ApplicationRecord
has_many :comments
accepts_nested_attributes_for :comments
end
親モデルにてaccepts_nested_attributes_for :comments
を指定し、子モデルのデータを受け入れるようにしておくことがポイントです。
子モデル(Comment)
子モデルの記載には特に変わったことはありません。
class Comment < ApplicationRecord
belongs_to :shop
end
コントローラー
親モデル側のコントローラーに、工夫すべき点がいくつかあります。
class ShopsController < ApplicationController
def new
@shop = Shop.new
@shop.comments.build # ←一つ目のポイント
end
def create
@shop = Shop.create(shop_params)
if @shop.save
# 成功したときの処理
else
# 成功しなかったときの処理
end
end
private
def shop_params # ↓2つ目のポイント
params.require(:shop).permit(:name, comments_attributes: [:content])
end
end
一つ目のポイントは、new
アクションのところで、@shop.comments.build
と記載し、子テーブルであるcomments
のインスタンスをbuild
までしておくことです。こうすることで、親テーブルに紐づくcomments
を作成する準備ができます。
二つ目のポイントは、shop_params
のところで、comments_attributes: [:content]
と記載し、子テーブルの要素もpermitしておくことです。要素が二つ以上あるときには、comments_attributes: [:content, :attr2, :attr3]
のように記載します。
ビュー
ビューには、このように記載します。
= form_with model: @shop, local: true do |f|
= f.label :name, "店名"
= f.text_field :name
= f.fields_for :comments, local: true do |comments_form|
= comments_form.label :content, "ショップへのコメント"
= comments_form.text_field :content
= f.submit "送信"
これで、子テーブルのデータも一気に作成されるはずです。
余談ですが、以前f.fields_for
のところをfields_for
と書いてしまい、子テーブルの要素がshop_params
として送られずはまったことがありました。。。
f.fields_for
と記載することで、親テーブルのフォームオブジェクトに紐づけることができます。
テスト
テストもそれなりにコツが必要だったので記載します。ここでは、リクエストspecのみを書きます。
factory
親(shop)
FactoryBot.define do
factory :shop do
name { "テストのお店" }
trait :with_nested_instances do
after( :create ) do |shop|
create :comment, id: shop.id
end
end
end
end
trait
といのは、FactoryBotで、状況により少しだけ違うデータを用いたい場合の設定方法です。
after(:create)
はFactoryBotのコールバックです。この辺りはこちらの記事が参考になりました。
子(comment)
FactoryBot.define do
factory :comment do
content { "コメント" }
end
end
子モデルのfactoryに特に変わった点はありません。
リクエストspec
RSpec.describe "Shops", type: :request do
describe "POST /shops" do
context "依存関係のあるテーブルのparamsが送信されているとき" do
before do
@comment_params = {
comments_attributes: {
"0": FactoryBot.attributes_for(:comment)
}
}
@params_nested = {
shop: FactoryBot.attributes_for(:shop).merge( @comment_params )
}
end
it 'リクエストが成功すること' do
post shops_url, params: @params_nested
expect(response.status).to eq 302
end
it "ショップ情報とコメントが新規作成されること" do
expect do
post shops_url, params: @params_nested
end.to change(Shop, :count).by(1) and change(Comment, :count).by(1)
end
end
end
end
"0"
あたりがちょっと特殊な書き方に見えますが、これは、実際のデータ送信時に送られるparams
に合わせた形になります。長かったですが、テストまで頑張ったおかげで、依存関係のあるデータの作成と大分仲良くなれました。
参考記事
ここまでに参考にした記事はこちらです。
RSpecにおけるFactoryGirlの使い方まとめ
【Rails】複数のレコードを作成する。modelの関係性によって異なるform_for / fields_forの使い方
↓特にこの記事にはお世話になりました!!
[Rails]accepts_nested_attributes_forの使い方
追記(発展学習?)
結局は自分の凡ミスだったのですが、同様にネストしたテーブルについて悩んていたときにスクールの同窓生コミュニティに投稿したら、以下のことを教えていただきました。
▼よくわからないがフォームがすっきり書けるらしい(そんな理解ですみません)
【Rails】FormObjectを使ってほしい
▼次のフォームには使ってみたいと思っているgem, 1体多の構造を持つテーブルで、多数の子テーブルデータが一気に作成できるやつ
fields_forを使った子モデルへの複数レコード保存【cocoonが便利】
では、今日もこれから仕事、頑張ります。アドバイスくださった皆様、ありがとうございました!!