Railsで子レコードを親レコードと同時に作成する仕組みはネットにあったが、孫まで一緒に作るという記事がなかったのでまとめます。
accepts_nested_attributes_forを使って、親子孫の関係のレコードを作成することを目標にします。
目次
今回やりたいこと
- 親レコードと同時に、子孫関係であるレコードも同時に作成する(編集も同じ要領でできるはず)
実装・テスト詳細
- Menu-Food-Materialという親子孫の関連を作る
- field_forを使って1つのフォームから登録できるようにする
- テストはrspec(controllerのテスト)
- データ作成はFactorybot
環境
- 仮想環境centos7
- rails 5.1.6
- postgresql 9.2.24
手順
実装
1:controller/modelなど前準備
$ rails g scaffold Menu menu_name $ rails g model Food food_name menu:references $ rails g model Material material_name food:references $ rake db:migrate
# models/menu.rb class Menu < ApplicationRecord has_many :foods, inverse_of: :menu accepts_nested_attributes_for :foods end
# models/food.rb class Food < ApplicationRecord belongs_to :menu, inverse_of: :foods has_many :materials, inverse_of: :food accepts_nested_attributes_for :materials end
# models/material.rb class Material < ApplicationRecord belongs_to :food, inverse_of: :materials end
親子孫の関係なので、上記のように順にhas_mayとaccepts_nested_attributes_forを設定していきます。
ここまで来たら、ある程度の準備はできたのでcontroller/viewの実装へ。
3:controller/view実装
# controllers/menus_controller.rb def new @menu = Menu.new food = @menu.foods.build food.materials.build end def create @menu = Menu.create(menu_params) end . . . private def menu_params params.require(:menu).permit(:menu_name, foods_attributes: [ :food_name, :_destroy, materials_attributes: %i(material_name, _destroy) ] ) end
newアクションでbuildを使うのは慣習。
ここストロングパラメータで、foods_attributesのように子関係のパラメータを受け取る。foods_attributesの「foods」の部分はviewのfields_forで指定した名前を使う。
# views/menus/_form.html.erb <%= form_for @menu do |menu| %> <div class="field"> <%= menu.label :menu_name %> <%= menu.text_field :menu_name %> </div> <div class="field"> <%= menu.fields_for :foods do |food| %> <%= food.label :food_name %> <%= food.text_field :food_name %> <%= food.fields_for :materials do |material| %> <%= material.label :material_name %> <%= material.text_field :material_name %> <% end %> <% end %> </div> <div class="actions"> <%= menu.submit %> </div> <% end %>
fields_forを使うと、foodとmaterialのモデルオブジェクトを作成できる。
テスト
1:factorybotでデータ作成
# spec/factories/menus.rb FactoryBot.define do factory :menu do menu_name "MyString" end end
# spec/factories/foods.rb FactoryBot.define do factory :food do food_name "MyString" end end
# spec/factories/materials.rb FactoryBot.define do factory :material do material_name "MyString" end end
2:newテスト/createテスト
# spec/controllers/menu_controller_spec.rb require 'rails_helper' RSpec.describe MenusController, type: :controller do let(:menu) {attributes_for(:menu)} let(:food) {attributes_for(:food)} let(:material) {attributes_for(:material)} describe '新規作成について' do it '#new' do get :new expect(response).to have_http_status(200) end it '#create' do food["materials_attributes"] = {"0" => material} menu["foods_attributes"] = {"0" => food} expect { post :create, menu: menu }.to change(Menu, :count).by(1).and change(Food, :count).by(1).and change(Material, :count).by(1) end end end
ポイント・キーワード
この記事のポイント・キーワード
- modelにinverse_ofを使うことで、双方向の関連付け
- accepts_nested_attributes_forを指定して、入れ子フォームを使えるようにする
- field_forで入れ子のモデルオブジェクトを扱う
- field_forで指定した[モデルオブジェクト名]+_attributesでパラメータが送られるからストロングパラメータも注意
- newアクションで、インスタンスを生成する時newじゃなくbuildを使うのは関連付けられたインスタンスを作るときの慣習
コメントを残す