Nested attributes
Nested attributes allows you to store the properties of child objects using a parent object. By default, the nested attribute will be disabled, you can enable it for use with the class method accepts_nested_attributes_for
. When you enable the nested attribute an attribute writer
will be defined in the model. After the attribute writer
been defined, in the following example, two new methods will be added to model author_attributes=(attributes)
and pages_attributes=(attributes)
1 2 3 4 5 6 7 | class Book < ActiveRecord::Base has_one :author has_many :pages accepts_nested_attributes_for :author, :pages end |
Note :autosave
option will be automatically added to associations that are accepts_nested_attributes_for
One-to-one
Let’s look at the example: Member model has an Avatar
1 2 3 4 5 | class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar end |
Using nested attributes for one-to-one association allows you to create a new Member with Avatar with just one create:
1 2 3 4 5 | params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } } member = Member.create(params[:member]) member.avatar.id # => 2 member.avatar.icon # => 'smiling' |
It allows you to update your avatar through members:
1 2 3 4 | params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } } member.update params[:member] member.avatar.icon # => 'sad' |
If you want to update your avatar without providing its id, you must add the option :update_only
:
1 2 3 4 5 6 7 8 9 10 | class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar, update_only: true end params = { member: { avatar_attributes: { icon: 'sad' } } } member.update params[:member] member.avatar.id # => 2 member.avatar.icon # => 'sad' |
By default, you can only set and update properties in the model. If you want to destroy association properties, you must enable it with :allow_destroy
option:
1 2 3 4 5 | class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar, allow_destroy: true end |
Then, when you add a _destroy
key with a true
value to the attributes hash, the associated model is deleted:
1 2 3 4 5 | member.avatar_attributes = { id: '2', _destroy: '1' } member.avatar.marked_for_destruction? # => true member.save member.reload.avatar # => nil |
Note: The model will not be deleted unless you save the parent model (member.save).
One-to-many
Consider the example: A Member has many posts:
1 2 3 4 5 | class Member < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts end |
You can now set or update associated posts by adding the key :posts_attributes
whose value is the array of hashes as post attributes:
1 2 3 4 5 6 7 8 9 10 11 12 13 | params = { member: { name: 'joe', posts_attributes: [ { title: 'Kari, the awesome Ruby documentation browser!' }, { title: 'The egalitarian assumption of the modern citizen' }, { title: '', _destroy: '1' } # this will be ignored ] }} member = Member.create(params[:member]) member.posts.length # => 2 member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' member.posts.second.title # => 'The egalitarian assumption of the modern citizen' |
You can also add :reject_if
to reject records if it doesn’t meet its condition. So the above example can be rewritten as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Member < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? } end params = { member: { name: 'joe', posts_attributes: [ { title: 'Kari, the awesome Ruby documentation browser!' }, { title: 'The egalitarian assumption of the modern citizen' }, { title: '' } # this will be ignored because of the :reject_if proc ] }} member = Member.create(params[:member]) member.posts.length # => 2 member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' member.posts.second.title # => 'The egalitarian assumption of the modern citizen' |
You can also pass :reject_if
a symbol to call a method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Member < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts, reject_if: :new_record? end class Member < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts, reject_if: :reject_posts def reject_posts(attributes) attributes['title'].blank? end end |
If the passed id matches an existing record, it will be overwritten and edited:
1 2 3 4 5 6 7 8 9 10 11 | member.attributes = { name: 'Joe', posts_attributes: [ { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' }, { id: 2, title: '[UPDATED] other post' } ] } member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' member.posts.second.title # => '[UPDATED] other post' |
However, this will only work if the parent model has been created. For example, if you want to create a member
with the name “joe” and update posts
at the same time it will raise the error ActiveRecord::RecordNotFound
By default, associated records are protected from being deleted. If you want to remove a associated record, you must enable it first using the allow_destroy: true
option:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Member < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts, allow_destroy: true end params = { member: { posts_attributes: [{ id: '2', _destroy: '1' }] }} member.attributes = params[:member] member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true member.posts.length # => 2 member.save member.reload.posts.length # => 1 |
Properties for a associated can also be written as a string of hash instead of a hash array.
1 2 3 4 5 6 7 8 | Member.create( name: 'joe', posts_attributes: { first: { title: 'Foo' }, second: { title: 'Bar' } } ) |
has the same effect as:
1 2 3 4 5 6 7 8 | Member.create( name: 'joe', posts_attributes: [ { title: 'Foo' }, { title: 'Bar' } ] ) |
Validating the presence of a parent model
You can use the validates_presence_of
method and :inverse_of
key to validate a child record associated with a parent record.
1 2 3 4 5 6 7 8 9 10 | class Member < ActiveRecord::Base has_many :posts, inverse_of: :member accepts_nested_attributes_for :posts end class Post < ActiveRecord::Base belongs_to :member, inverse_of: :posts validates_presence_of :member end |
Note that, if you don’t specify the :inverse_of
option, the Active Record will automatically guess the inverse association. For one-to-one nested associations, if you create (in-memory) a sub-object before assigning it, that module will not be overwritten:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar def avatar super || build_avatar(width: 200) end end member = Member.new member.avatar_attributes = {icon: 'sad'} member.avatar.width # => 200 |