はじめに
accepts_nested_attributes_forメソッドがあまりに初心者殺しというか、製作者のDHHさんにこのメソッドを
抹殺したい・・・
と言わせるくらいの極悪メソッドであるのですが、現在はこのメソッドを使わざるを得ない状況なので初学者の方を救うべく記事をまとめます。
accepts_nested_attributes_forメソッドとは
accepts_nested_attributes_forメソッドは
モデル同士が関連付けられている時に、ネストさせることで一度にまとめてレコードの更新ができるようにするメソッドです。
公式
Active Record Nested Attributes
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
前提
公式に則ってモデルやコントローラ、ビューを定義しておきます!
bookモデル
Column | Type | Option |
---|---|---|
name | string | null: false, unique: true |
author | references | null: false, foreign_key: true |
page | references | null: false, foreign_key: true |
Association
has_one: author
has_many: pages
accepts_nested_attributes_for :author, :pages
authorモデル
Column | Type | Option |
---|---|---|
name | string | null: false, unique: true |
Association
belongs_to: book
pageモデル
Column | Type | Option |
---|---|---|
name | string | null: false, unique: true |
Association
belongs_to: book
コントローラー
# in app/controllers/books_controller.rb
class BooksController < ApplicationController
# ...省略
def create
# ...省略
end
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to book_path(@book), notice: '更新しました'
else
render :edit
end
end
def book_params
params.require(:book).permit(:name, book_attributes: [:id, :name, :_destroy])
end
end
viewファイル
# in app/views/books/new.html.erb
<%= form_with @book do |f| %>
<%= f.text_field :name %>
<%= f.field_for :関連付けているモデル名 |g| %>
<%= g.text_field :関連付けているモデルのカラム名 %>
削除する <%= g.check_box :_destroy %>
<% end %>
<% f.submit %>
<% end %>
1対1のアソシエーションの場合
抽象的な書き方
# in app/models/モデル名.rb
class モデル名 < ActiveRecord::Base
has_one :関連づけているモデル名
accepts_nested_attributes_for :関連づけているモデル名
end
具体的な書き方
# in app/models/book.rb
class Book < ActiveRecord::Base
has_one :author
accepts_nested_attributes_for :author
end
ターミナル
params = { book: { name: 'Harry Potter', author_attributes: { name: 'J・k・Rowling' } } }
book = Book.create(params[:book])
book.author.id # => 2
book.author.name # => 'J・k・Rowling'
params = { book: { author_attributes: { id: '2', name: 'Joanne Rowling' } } }
book.update params[:book]
book.author.name # => 'Joanne・Rowling'
オプションについて
update_onlyオプション
新たに作成することなく、データの更新だけしたい場合につけます。
class book < ActiveRecord::Base
has_one :author
accepts_nested_attributes_for :author, update_only: true
end
ターミナル
params = { book: { author_attributes: { name: 'Joanne Rowling' } } }
book.update params[:book]
book.author.id # => 2
book.author.name # => 'Joanne・Rowling'
allow_destroyオプション
親要素が削除された時、関連付けている情報もまとめて削除するためのオプションです。
基本的にはあったほうが良いオプションですね。
例)何かのサイトで退会したのにずっと関連情報が保持されてしまい、データベースを圧迫してしまう。
class book < ActiveRecord::Base
has_one :author
accepts_nested_attributes_for :author, allow_destroy: true
end
ターミナル
book.author_attributes = { id: '2', _destroy: '1' }
book.author.marked_for_destruction? # => true
book.save
book.reload.author # => nil
1対多のアソシエーションの場合
抽象的な書き方
# in app/models/モデル名.rb
class モデル名 < ActiveRecord::Base
has_many :関連づけているモデル名
accepts_nested_attributes_for :関連づけているモデル名
end
具体的な書き方
渡されたデータ(ハッシュ)に_destroy属性がtrueで入っていなければ、新しいidが付与されて関連データが作成されます。
下の例だと、_destroy: ‘1’がついているものは弾かれます。
# in app/models/book.rb
class Book < ActiveRecord::Base
has_many :pages
accepts_nested_attributes_for :pages
end
ターミナル
params = { book: {
name: 'Harry Potter', pages_attributes: [
{ name: 'first' },
{ name: 'second' },
{ name: '', _destroy: '1' } # this will be ignored
]
}}
book = Book.create(params[:book])
book.pages.length # => 2
book.pages.first.name # => 'first'
book.pages.second.name # => 'second'
allow_destroyオプション
1対1のアソシエーションの時と同じ流れです!!
class Book < ActiveRecord::Base
has_many :pages
accepts_nested_attributes_for :pages, allow_destroy: true
end
params = { book: {
pages_attributes: [{ id: '2', _destroy: '1' }]
}}
book.attributes = params[:book]
book.pages.detect { |p| p.id == 2 }.marked_for_destruction? # => true
book.pages.length # => 2
book.save
book.reload.pages.length # => 1
reject_ifオプション
弾きたいものがある場合、procを用いて指定することもできます。
下記の例だと、空の文字列が入っているデータを弾いています。
class Book < ActiveRecord::Base
has_many :pages
accepts_nested_attributes_for :pages, reject_if: proc { |attributes| attributes['title'].blank? }
end
ターミナル
params = { book: {
name: 'Harry Potter', pages_attributes: [
{ name: 'first' },
{ name: 'second' },
{ name: '' } # this will be ignored
]
}}
book = Book.create(params[:book])
book.pages.length # => 2
book.pages.first.name # => 'first'
book.pages.second.name # => 'second'
終わりに
公式を整理するだけでもかなりのボリュームになってしまいました。
しかし、基本的な書き方をマスターすれば応用が効きますので、オプションなどはしっかりと把握しておきましょう!