#目的
関連を持ったモデルをまとめて操作するために使われるaccepts_nested_attributes_forの使い方を整理する
・導入
・サンプルコード
・テスト
ついでにcocoonについても簡単に使えるようにする
#環境
Ruby 2.2.2 p95
Rails 4.2.6
RSpec 3.4.4
FactoryGirl 4.7.0
なおこのソースコードはGithubに置いてあります。
#事前準備
##環境
Railsアプリを作って、
$ rails new hoge
Gemfile更新して、
※cocoonはフィールドを動的に生成するところで使っています。
※rspecやfactory_girlなどはテストに必要なgemなので、テストを書かないとか、違うツールを使う場合は省略して構わないです。
gem 'cocoon'
group :development, :test do
gem 'rspec-rails'
gem 'factory_girl_rails'
end
group :test do
gem 'capybara'
end
bundle install
しつつ、RSpecをインストール。
$ bundle install
$ rails generate rspec:install
cocoon用の設定をapplication.jsに追加
//= require cocoon
##モデル
とりあえずモデルを2つつくります。
親は全部欲しいのでscaffoldで、子はモデルのみ欲しいので個別につくります。
$ rails generate scaffold parent name:string
$ rails generate model child name:string parent:references
親にはhas_many :children
を追加します。
親が削除された時に合わせて子も削除されるようdependent: :destroy
もつけます。
accepts_nested_attributes_for
を使うと、親のformに子のフィールドを追加するだけで一気にcreate, updateできるようになります。
削除フラグで削除できるようallow_destroy: true
をつけます。
class Parent < ActiveRecord::Base
has_many :children, dependent: :destroy
accepts_nested_attributes_for :children, allow_destroy: true
end
子(models/child.rb)はそのままで良いです。
migrationします。
$ rake db:migrate
#コード
このままだと親しかつくれないので、親と一緒に子を作れるようにします。
##View
こんな感じでフィールドを追加します。
association_insertion_method
はcocoonのメソッドで行を動的に追加するものです。
チェックボックスは削除のためのものです。(cocoonのlink_to_remove_associationを使っても良いかと思います)
<%= form_for(@parent) do |f| %>
<!-- 中略 -->
<div class="field">
<%= f.fields_for :children do |c| %>
<%= render 'child_fields', f: c %>
<% end %>
</div>
<div class="field">
<%= link_to_add_association '子を追加', f, :children,
data: { association_insertion_method: 'before' } %>
</div>
<!-- 中略-->
<% end %>
子のフィールドを個別に用意します。
_モデル名_fields
という名前をつけるとcocoonが自動的に読み込んでくれます。
<div class="field">
<%= f.label 'Child name' %><br>
<%= f.text_field :name %>
<% if @parent.persisted? %>
<%= f.label '削除' %>
<%= f.check_box :_destroy %>
<% end %>
<br>
</div>
##Controller
###strong parameters
Childモデルのattributeをstrong parametersに追加しておきます。
削除フラグを利用するため、_destroy
も追加します。
_destroyがtrueだと削除が実行されます。
private
def parent_params
params.require(:parent).permit( :name, children_attributes:[ :id, :name, :parent_id, :_destroy ] )
end
以上。
これで一括生成できます。
#テスト
テストの仕方(自分が書きたかったのはここだったりする)
##FactoryGirl
親子それぞれのfactoryと、子も含んだ親factoryを用意します。
FactoryGirl.define do
factory :parent do
name "MyString"
end
factory :parent_with_child, class: Parent do
name "MyString"
after( :create ) do |parent|
create :child, parent: parent
end
end
end
FactoryGirl.define do
factory :child do
name "MyString"
parent nil
end
end
##RSpec
###controller
attributes_for
でattributeのみ取り出して、mergeしてpost/patchします。
RSpec.describe ParentsController, type: :controller do
describe "POST #create" do
context "with valid params" do
it "creates a new Parent" do
params = { children_attributes: [ FactoryGirl.attributes_for( :child ) ] }
expect {
post :create,
parent: FactoryGirl.attributes_for( :parent ).merge( params )
}.to change( Parent, :count ).by( 1 ).and change( Child, :count ).by( 1 )
end
end
end
###feature
まずはfeatureテストを作って、
$ rails g rspec:feature patch
parent_with_childを使い、一括でcreateできます。
RSpec.feature "Patches", type: :feature do
scenario 'patch test' do
parent = FactoryGirl.create( :parent_with_child )
expect( Parent.count ).to eq 1
expect( Child.count ).to eq 1
end
end
#参考
関連について
[Rails] 親ー子ー孫と関係のあるモデルを一括で削除する
フォームの動的生成について
nested_form はもう古い!? Cocoon で作る1対多のフォーム
Qiitaのnested_formの検索結果
FactoryGirlについて
(゚∀゚)o彡 sasata299's blog - factory_girl で最低限知っておきたい4つの使い方