Rails Active Record Nested Attributes
This post hasn't been updated for 4 years
Nested attributes
Nested attributes cho phép bạn lưu các thuộc tính của các object con bằng parent object. Mặc định thì nested attribute sẽ bị disable, bạn có thể enable nó để sử dụng bằng class method accepts_nested_attributes_for
. Khi bạn enable nested attribute thì một attribute writer
sẽ được định nghĩa trong model.
Sau khi attribute writer
được định nghĩa, theo ví dụ dưới đây thì hai method mới sẽ được thêm vào model author_attributes=(attributes)
và pages_attributes=(attributes)
class Book < ActiveRecord::Base
has_one :author
has_many :pages
accepts_nested_attributes_for :author, :pages
end
Chú ý: :autosave
option sẽ được tự động thêm vào các association mà được sử dụng accepts_nested_attributes_for
One-to-one
Cùng xem xét ví dụ: model Member có một Avatar
class Member < ActiveRecord::Base
has_one :avatar
accepts_nested_attributes_for :avatar
end
Sử dụng nested attributes cho one-to-one association cho phép bạn tạo mới một Member với Avatar chỉ với một lần create:
params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
member = Member.create(params[:member])
member.avatar.id # => 2
member.avatar.icon # => 'smiling'
Nó cho phép bạn update avatar thông qua member:
params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
member.update params[:member]
member.avatar.icon # => 'sad'
Nếu bạn muốn update avatar mà không cần cung cấp id của nó thì bạn phải thêm option :update_only
:
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'
Mặc định, bạn chỉ có thể set và update các thuộc tính trong model. Nếu bạn muốn destroy các thuộc tính association, bạn phải enable nó bằng :allow_destroy
option:
class Member < ActiveRecord::Base
has_one :avatar
accepts_nested_attributes_for :avatar, allow_destroy: true
end
Khi đó, khi bạn thêm _destroy
key với giá trị true
vào attributes hash thì associated model sẽ bị xóa:
member.avatar_attributes = { id: '2', _destroy: '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil
Chú ý: Model sẽ không bị xóa trừ khi bạn lưu model cha (member.save).
One-to-many
Xét ví dụ: Một Member có nhiều bài posts:
class Member < ActiveRecord::Base
has_many :posts
accepts_nested_attributes_for :posts
end
Bây giờ bạn có thể set hoặc update associated posts bằng cách thêm key :posts_attributes
với giá trị là mảng các hash là các thuộc tính của post:
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'
Bạn cũng có thể thêm :reject_if
để loại bỏ các record nếu nó không thỏa mãn điều kiện của nó. Như vậy ví dụ ở trên có thể viết lại như sau:
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'
Bên cạnh đó bạn cũng có thể truyền vào :reject_if
một symbol để gọi đến một method:
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
Nếu id truyền vào mà trùng với một record đã tồn tại thì nó sẽ được ghi đè và chỉnh sửa lại:
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'
Tuy nhiên, trường hợp trên chỉ chạy nếu như parent model đã được tạo. Ví dụ, nếu bạn muốn tạo một member
với tên "joe" và update posts
ngay cùng thời điểm nó sẽ raise lỗi ActiveRecord::RecordNotFound
Mặc định thì các associated record được bảo vệ để không bị xóa. Nếu bạn muốn xóa một associated record, bạn phải enable nó trước bằng cách sử dụng option allow_destroy: true
:
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
Các thuộc tính cho một associated cũng có thể được viết dưới dạng một chuỗi hash thay vì một mảng hash.
Member.create(
name: 'joe',
posts_attributes: {
first: { title: 'Foo' },
second: { title: 'Bar' }
}
)
có tác dụng tương tự như:
Member.create(
name: 'joe',
posts_attributes: [
{ title: 'Foo' },
{ title: 'Bar' }
]
)
Validating the presence of a parent model
Bạn có thể sử dụng validates_presence_of
method và :inverse_of
key để validate một bản ghi con được liên kết với một bản ghi cha.
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
Chú ý rằng, nếu bạn không chỉ định cho :inverse_of
option thì Active Record sẽ tự động đoán inverse association.
Đối với one-to-one nested associations, nếu bạn tạo mới (in-memory) một object con trước khi gán nó, thì module đó sẽ không bị ghi đè:
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
Tài liệu tham khảo
All Rights Reserved