Railsでネストした関連先のテーブルもまとめて保存したい時に使うのが
accepts_nested_attributes_forです。すごく便利。
Railsのソースコードを見ながら使い方をまとめてみました。
コンソールから色々試してみる
定義する
class User < ActiveRecord::Base has_one :profile has_many :academics accepts_nested_attributes_for :profile accepts_nested_attributes_for :academics end
profile_attributes= と academics_attributes= メソッドが追加される
$ rails c
has_one の場合
# 登録時 > params = { user: { name: 'Test', profile_attributes: { nickname: 'test' } } } > user = User.create(params[:user]) > user.profile.id # => 1 > ser.profile.nickname # => 'test' # 更新時 > params = { user: { profile_attributes: { id: '1', nickname: 'test2' } } } > user = User.find 1 > user.update params[:user] > user.profile.nickname # => 'test2'
has_many の場合
> params = { user: { name: 'Test', academics_attributes: [ { school_name: 'test school1' }, { school_name: 'test school2' } ] }} > user = User.create(params[:user]) > user.academics.length # => 2 > user.academics.first.school_name # => 'test school1' > user.academics.second.school_name # => 'test school2'
保存時に関連先のレコードを削除したい場合
削除するには allow_destroy オプションを使う
accepts_nested_attributes_for :profile, allow_destroy: true accepts_nested_attributes_for :academics, allow_destroy: true
削除したい時はビューから _destroy パラメーターにtrueをセットして送る
> user = User.find 1 > user.profile_attributes = { id: '1', _destroy: 'true' } > user.save > user.reload.profile # => nil
ビューから _destroy を渡せない場合は mark_for_destruction を使う
> user = User.find 1 > user.profile.mark_for_destruction > user.save > user.reload.profile # => nil
保存条件を指定する
accepts_nested_attributes_for :academics, reject_if: lambda { |attributes| attributes['school_name'].blank? }
> params = { user: { name: 'Test', academics_attributes: [ { school_name: 'test school1' }, { school_name: 'test school2' }, { school_name: '' } ] }} > user = User.create(params[:user]) > user.academics.length # => 2 > user.academics.first.school_name # => 'test school1' > user.academics.second.school_name # => 'test school2' > user.academics.third.nil? # => true
# 使用可能なメソッドはシンボルで指定することができる accepts_nested_attributes_for :academics, reject_if: :new_record? accepts_nested_attributes_for :academics, reject_if: :all_blank accepts_nested_attributes_for :academics, reject_if: :reject_academics def reject_academics(attributed) attributed['school_name'].blank? end
関連するレコードの最大数を指定する
accepts_nested_attributes_for :academics, limit: 2
> params = { user: { name: 'Test', academics_attributes: [ { school_name: 'test school1' }, { school_name: 'test school2' }, { school_name: 'test school3' } ] }} > User.create(params[:user]) # => ActiveRecord::NestedAttributes::TooManyRecords: Maximum 2 records are allowed. Got 3 records instead.
Strong Parameters と一緒に使う
def user_params params.require(:user).permit(:name, profile_attributes: [:nickname], academics_attributes: [:school_name]) end
実際に使ってみる
準備
$ rails g scaffold user name:string $ rails g model User::Profile user_id:integer nickname:string $ rails g model User::Academic user_id:integer school_name:string $ rake db:migrate
モデル
User モデルを保存する時に User::Profile モデルと User::Academic モデルもまとめて保存したい時はこのようにモデルに定義してあげる。
# app/models/user.rb class User < ActiveRecord::Base has_one :profile has_many :academics accepts_nested_attributes_for :profile accepts_nested_attributes_for :academics end
ユーザー登録の場合
# app/controllers/users_controller.rb class UsersController < ApplicationController def new @user = User.new @user.build_profile # 追加 2.times { @user.academics.build } #追加 end def create @user = User.new(user_params) if @user.save redirect_to @user, notice: 'User was successfully created.' else render action: 'new' end end private def user_params params.require(:user).permit(:name, profile_attributes: [:nickname], academics_attributes: [:school_name]) # パラメーターを追加 end end
# app/views/users/_form.html.erb <%= form_for(@user) do |f| %> <%# 追加 %> <%= f.fields_for :profile do |profile_fields| %> <%= profile_fields.text_field :nickname %> <% end %> <%# 追加 %> <%= f.fields_for :academics do |academics_fields| %> <%= academics_fields.text_field :school_name %> <% end %> <% end %>
生成されるHTML
<input id="user_profile_attributes_nickname" name="user[profile_attributes][nickname]" type="text"> <input id="user_academics_attributes_0_school_name" name="user[academics_attributes][0][school_name]" type="text"> <input id="user_academics_attributes_1_school_name" name="user[academics_attributes][1][school_name]" type="text">
ログを見るとちゃんと保存されている。
Parameters: {..., "user"=>{"name"=>"hoge", "profile_attributes"=>{"nickname"=>"piyo"}, "academics_attributes"=>{"0"=>{"school_name"=>"school1"}, "1"=>{"school_name"=>"school2"}}}, ...} (0.2ms) BEGIN SQL (0.3ms) INSERT INTO `users` (`created_at`, `name`, `updated_at`) VALUES ('2013-06-30 07:37:09', 'hoge', '2013-06-30 07:37:09') SQL (0.3ms) INSERT INTO `user_profiles` (`created_at`, `nickname`, `updated_at`, `user_id`) VALUES ('2013-06-30 07:37:09', 'piyo', '2013-06-30 07:37:09', 2) SQL (0.3ms) INSERT INTO `user_academics` (`created_at`, `school_name`, `updated_at`, `user_id`) VALUES ('2013-06-30 07:37:09', 'school1', '2013-06-30 07:37:09', 2) SQL (0.3ms) INSERT INTO `user_academics` (`created_at`, `school_name`, `updated_at`, `user_id`) VALUES ('2013-06-30 07:37:09', 'school2', '2013-06-30 07:37:09', 2) (0.3ms) COMMIT
ユーザー編集の場合
編集画面で生成されるHTML
<input id="user_profile_attributes_nickname" name="user[profile_attributes][nickname]" type="text" value="piyo"> <input id="user_profile_attributes_id" name="user[profile_attributes][id]" type="hidden" value="2"> <input id="user_academics_attributes_0_school_name" name="user[academics_attributes][0][school_name]" type="text" value="school1"> <input id="user_academics_attributes_0_id" name="user[academics_attributes][0][id]" type="hidden" value="3"> <input id="user_academics_attributes_1_school_name" name="user[academics_attributes][1][school_name]" type="text" value="school2"> <input id="user_academics_attributes_1_id" name="user[academics_attributes][1][id]" type="hidden" value="4">
更新するためには「id」属性を受け入れるようにする必要があります。 忘れると新規レコード扱いされ更新することができません。
# app/controllers/users_controller.rb def user_params params.require(:user).permit(:name, profile_attributes: [:id, :nickname], academics_attributes: [:id, :school_name]) end
ログを確認するとちゃんと更新されている
Parameters: {..., "user"=>{"name"=>"hoge", "profile_attributes"=>{"nickname"=>"_piyo", "id"=>"2"}, "academics_attributes"=>{"0"=>{"school_name"=>"_school1", "id"=>"3"}, "1"=>{"school_name"=>"_school2", "id"=>"4"}}}, "id"=>"2", ...} User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 (0.1ms) BEGIN User::Profile Load (0.9ms) SELECT `user_profiles`.* FROM `user_profiles` WHERE `user_profiles`.`user_id` = 2 ORDER BY `user_profiles`.`id` ASC LIMIT 1 User::Academic Load (0.4ms) SELECT `user_academics`.* FROM `user_academics` WHERE `user_academics`.`user_id` = 2 AND `user_academics`.`id` IN (3, 4) SQL (1.5ms) UPDATE `user_profiles` SET `nickname` = '_piyo', `updated_at` = '2013-06-30 07:42:52' WHERE `user_profiles`.`id` = 2 SQL (0.4ms) UPDATE `user_academics` SET `school_name` = '_school1', `updated_at` = '2013-06-30 07:42:52' WHERE `user_academics`.`id` = 3 SQL (0.3ms) UPDATE `user_academics` SET `school_name` = '_school2', `updated_at` = '2013-06-30 07:42:52' WHERE `user_academics`.`id` = 4 (0.5ms) COMMIT
ユーザー情報を保存する場合関連先の情報を削除したい場合
削除するには allow_destroy オプションを使う
# app/models/user.rb class User < ActiveRecord::Base ... accepts_nested_attributes_for :profile, allow_destroy: true accepts_nested_attributes_for :academics, allow_destroy: true end
ビューから削除用のパラメーター(_destroy)を送れるようにする
app/views/users/_form.html.erb <%= f.fields_for :profile do |profile_fields| %> <%= profile_fields.text_field :nickname %> <%= profile_fields.check_box :_destroy %> <% end %> <%= f.fields_for :academics do |academics_fields| %> <%= academics_fields.text_field :school_name %> <%= academics_fields.check_box :_destroy %> <% end %>
コントローラーは削除用のパラメーター(_destroy)を受け入れるようにする
# app/controllers/users_controller.rb def user_params params.require(:user).permit(:name, profile_attributes: [:id, :_destroy, :nickname], academics_attributes: [:id, :_destroy, :school_name]) end
生成されるHTML
<input name="user[profile_attributes][_destroy]" type="hidden" value="0" /> <input id="user_profile_attributes__destroy" name="user[profile_attributes][_destroy]" type="checkbox" value="1" /> <input name="user[academics_attributes][0][_destroy]" type="hidden" value="0" /> <input id="user_academics_attributes_0__destroy" name="user[academics_attributes][0][_destroy]" type="checkbox" value="1" /> <input name="user[academics_attributes][1][_destroy]" type="hidden" value="0" /> <input id="user_academics_attributes_1__destroy" name="user[academics_attributes][1][_destroy]" type="checkbox" value="1" />
ログ
Parameters: {..., "user"=>{"name"=>"hoge", "profile_attributes"=>{"nickname"=>"_piyo", "_destroy"=>"1", "id"=>"2"}, "academics_attributes"=>{"0"=>{"school_name"=>"_school1", "_destroy"=>"1", "id"=>"3"}, "1"=>{"school_name"=>"_school2", "_destroy"=>"1", "id"=>"4"}}}, "id"=>"2", ...} User Load (1.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 (0.4ms) BEGIN User::Profile Load (1.1ms) SELECT `user_profiles`.* FROM `user_profiles` WHERE `user_profiles`.`user_id` = 2 ORDER BY `user_profiles`.`id` ASC LIMIT 1 User::Academic Load (5.4ms) SELECT `user_academics`.* FROM `user_academics` WHERE `user_academics`.`user_id` = 2 AND `user_academics`.`id` IN (3, 4) SQL (6.0ms) DELETE FROM `user_profiles` WHERE `user_profiles`.`id` = 2 SQL (0.6ms) DELETE FROM `user_academics` WHERE `user_academics`.`id` = 3 SQL (0.6ms) DELETE FROM `user_academics` WHERE `user_academics`.`id` = 4 (2.5ms) COMMIT
おまけ
# has_many の場合はインスタンスを指定することもできる <% f.object.academics.each do |academic| %> <%= f.fields_for :academics, academic do |academics_fields| %> <%= academics_fields.text_field :school_name %> <% end %> <% end %> # has_many の場合は配列を渡すこともできる <%= f.fields_for :academics, f.object.academics do |academics_fields| %> <%= academics_fields.text_field :school_name %> <% end %>