【Rails】fields_for と accepts_nested_attributes_for

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 %>