Rails Active Record Nested Attributes

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