1. Concepts
Nested attibutes are an Active Record feature that supports creating new records through its parent record. By default, the feature is not enabled, if we want to use it, we must declare in the Model we want to perform. Activate it using the method:
accepts_nested_attributes_for
1 2 3 4 5 6 7 | class Book < ActiveRecord::Base has_one :author has_many :pages accepts_nested_attributes_for :author, :pages end |
accepts_nested_attributes_for :model_name
in the above example will create two new methods: author_attributes=(attributes)
and pages_attributes=(attributes)
Option
:autosave
will automatically be enabled on every link that accepts_nested_attributes_for is used
2. Usage
2.1. 1-1 relationship
1 2 3 4 5 | class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar end |
=> Only create only 1 time in a 1-1 relationship
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' |
=> You can also update avatar sub-records via member, note that parent records must be save
before child records can be save
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 :id
, you must add the option :update_only
1 2 3 4 5 | class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar, update_only: true end |
1 2 3 4 5 | params = { member: { avatar_attributes: { icon: 'sad' } } } member.update params[:member] member.avatar.id # => 2 member.avatar.icon # => 'sad' |
=> By default, only create
and update
on the linked model, if you want to destroy
through the parent record, we need to add the option :alow_destroy
, then add _destroy
to the attributes hash, with a value of true
then we can destroy child records.
1 2 3 4 5 6 7 8 9 10 | class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar, allow_destroy: true end member.avatar_attributes = { id: '2', _destroy: '1' } member.avatar.marked_for_destruction? # => true member.save member.reload.avatar # => nil |
The child records will not be destroyed if the parent record has not been saved
2.2. 1-n relationship
1 2 3 4 5 | class Member < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts end |
=> For each hash without the key :id
, a new record will be initialized, unless the hash also contains the key _destroy :true
1 2 3 4 5 6 7 8 | params = { member: { name: 'joe', posts_attributes: [ { title: 'Kari, the awesome Ruby documentation browser!' }, { title: 'The egalitarian assumption of the modern citizen' }, { title: '', _destroy: '1' } # bỏ qua ] }} |
1 2 3 4 5 | 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 |
=> We can also set the :reject_if
proc option to ignore any new record hashing functions if they do not meet our requirements. With the above example
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' |
=> some other way when used with :reject_if
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 hash contains the key :id
matches the linked record, the matching record will be update
1 2 3 4 5 6 7 8 9 10 | 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, the above applies only if the parent model is also being updated. For example, if you want to create a member named Joe and want to update the posts at the same time, it will cause an
ActiveRecord :: RecordNotFound
error.To destroy the child records, we do the same thing with a 1-1 relation
2.3. Commonly used options
:allow_destroy
Allow to delete the child records, passing_destro
y with a value of true will delete the child records. Records are deleted only when the parent record has been successfully saved.:reject_if
Allows to specify a Proc or a Symbol that points to a method that checks conditions, both of which returntrue/false
. When false:reject_if
not called, the nested attributes will be generated with full value for each field except_destroy
, when the condition returns false, the_destroy
field will be set totrue
to destroy the record.:limit
(1-n) Allows to specify the maximum number of child records that can be created in the parent record. If the number of child records exceeds the limit limit then raised exceptionNestedAttributes :: TooManyRecords
:update_only
(1-1) If you want to update a sub-record without adding:id
3. References
To be continued …
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
The first article still has many shortcomings, I hope you sympathize, the next article will guide you on how to use Nested Attributes
in practice. Thank you for scrolling here!