accepts_nested_attributes_forで親子孫関係のレコードを同時作成する方法【実装例・rspecあり】

Not image

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を使うのは関連付けられたインスタンスを作るときの慣習

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です