跳至内容 跳至搜索

Active Record 嵌套属性

嵌套属性允许您通过父级保存关联记录的属性。默认情况下,嵌套属性更新被关闭,您可以使用 accepts_nested_attributes_for 类方法启用它。启用嵌套属性后,会在模型上定义一个属性写入器。

属性写入器以关联命名,这意味着在以下示例中,您的模型将添加两个新方法

author_attributes=(attributes)pages_attributes=(attributes)

class Book < ActiveRecord::Base
  has_one :author
  has_many :pages

  accepts_nested_attributes_for :author, :pages
end

请注意,:autosave 选项会在使用 accepts_nested_attributes_for 的每个关联上自动启用。

一对一

考虑一个拥有一个头像的 Member 模型

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar
end

在一对一关联上启用嵌套属性允许您一次创建成员和头像

params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
member = Member.create(params[:member])
member.avatar.id # => 2
member.avatar.icon # => 'smiling'

它还允许您通过成员更新头像

params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
member.update params[:member]
member.avatar.icon # => 'sad'

如果您想在不提供 ID 的情况下更新当前头像,则必须添加 :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'

默认情况下,您只能设置和更新关联模型上的属性。如果您想通过属性哈希销毁关联模型,则必须首先使用 :allow_destroy 选项启用它。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar, allow_destroy: true
end

现在,当您将 _destroy 键添加到属性哈希中,其值为 true 时,您将销毁关联的模型

member.avatar_attributes = { id: '2', _destroy: '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil

请注意,在父级保存之前,模型不会被销毁。

还要注意,除非您在更新的哈希中指定了模型的 ID,否则模型不会被销毁。

一对多

考虑一个拥有多个帖子的成员

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts
end

您现在可以通过成员的属性哈希设置或更新关联帖子的属性:将键 :posts_attributes 与帖子属性哈希数组作为值一起包含。

对于没有id 键的每个哈希,将实例化一个新记录,除非该哈希还包含一个值为 true_destroy 键。

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'

您还可以设置一个 :reject_if proc 以静默忽略任何无法通过您的条件的新记录哈希。例如,前面的示例可以改写为

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'

或者,:reject_if 也接受一个用于方法的符号

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

如果哈希包含与已关联记录匹配的 id 键,则将修改匹配的记录

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'

但是,以上情况仅在父模型也正在更新时适用。例如,如果您想创建一个名为joemember,并想同时更新posts,那么会返回一个 ActiveRecord::RecordNotFound 错误。

默认情况下,关联记录受到保护,无法被销毁。如果您想通过属性哈希销毁任何关联记录,则必须首先使用 :allow_destroy 选项启用它。这将允许您使用 _destroy 键来销毁现有记录

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

关联集合的嵌套属性也可以以哈希的哈希形式传递,而不是哈希的数组

Member.create(
  name: 'joe',
  posts_attributes: {
    first:  { title: 'Foo' },
    second: { title: 'Bar' }
  }
)

Member.create(
  name: 'joe',
  posts_attributes: [
    { title: 'Foo' },
    { title: 'Bar' }
  ]
)

效果相同。

在这种情况下,:posts_attributes 的值的哈希的键会被忽略。但是,不允许对其中一个键使用 'id':id,否则哈希将被包装在一个数组中,并被解释为单个帖子的属性哈希。

以哈希的哈希形式传递关联集合的属性可以与从 HTTP/HTML 参数生成的哈希一起使用,在这些参数中,可能没有自然的方法来提交哈希的数组。

保存

对模型的所有更改(包括对那些被标记为要销毁的模型的销毁)在父模型保存时会自动且原子地保存和销毁。这发生在由父模型的保存方法发起的交易内部。请参阅 ActiveRecord::AutosaveAssociation

验证父模型的存在

class Veterinarian < ActiveRecord::Base
  has_many :patients, inverse_of: :veterinarian
  accepts_nested_attributes_for :patients
end

class Patient < ActiveRecord::Base
  belongs_to :veterinarian, inverse_of: :patients, optional: true
  validates :veterinarian, presence: true, unless: -> { awaiting_intake }
end

belongs_to 关联默认情况下会验证父模型的存在。您可以通过指定 optional: true 来禁用此行为。例如,这可以在有条件地验证父模型的存在时使用

请注意,如果您没有指定 :inverse_of 选项,那么 Active Record 将尝试根据启发式方法自动猜测反向关联。

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

对于一对一嵌套关联,如果您在赋值之前自己构建新的(内存中的)子对象,那么此模块不会覆盖它,例如

使用嵌套属性创建表单

使用 ActionView::Helpers::FormHelper#fields_for 为嵌套属性创建表单元素。

post members_path, params: {
  member: {
    name: 'joe',
    posts_attributes: {
      '0' => { title: 'Foo' },
      '1' => { title: 'Bar' }
    }
  }
}
Integration 测试参数应反映表单的结构。例如
方法

accepts_nested_attributes_for

常量 = REJECT_ALL_BLANK_PROC
 

proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }

实例公共方法

accepts_nested_attributes_for(*attr_names)

为指定的关联定义一个属性写入器。

支持的选项

:allow_destroy

如果为真,则销毁属性哈希中具有 _destroy 键和值为 true(例如 1、‘1’、true 或 ‘true’)的任何成员。此选项默认情况下为假。

:reject_if

允许您指定一个 Proc 或一个 Symbol,它指向一个方法,该方法检查是否应该为某个属性哈希构建记录。哈希被传递给提供的 Proc 或方法,它应该返回 truefalse。当没有指定 :reject_if 时,将为所有不具有值为 true 的 _destroy 的属性哈希构建记录。传递 :all_blank 而不是 Proc 将创建一个 proc,它将拒绝所有属性为空的记录,排除 _destroy 的任何值。

:limit

允许您指定可以使用嵌套属性处理的关联记录的最大数量。Limit 也可以指定为 Proc 或 Symbol,它指向一个应该返回数字的方法。如果嵌套属性数组的大小超过指定的限制,则会引发 NestedAttributes::TooManyRecords 异常。如果省略,则可以处理任意数量的关联。请注意,:limit 选项仅适用于一对多关联。

:update_only

对于一对一关联,此选项允许您指定当关联记录已存在时如何使用嵌套属性。通常,可以将现有记录更新为一组新的属性值,或者使用包含这些值的全新记录来替换它。默认情况下,:update_only 选项为假,嵌套属性仅在包含记录的 :id 值时用于更新现有记录。否则,将实例化一个新记录并用来替换现有记录。但是,如果 :update_only 选项为真,则嵌套属性始终用于更新记录的属性,而无论 :id 是否存在。该选项将被集合关联忽略。

# creates avatar_attributes=
accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? }
# creates avatar_attributes=
accepts_nested_attributes_for :avatar, reject_if: :all_blank
# creates avatar_attributes= and posts_attributes=
accepts_nested_attributes_for :avatar, :posts, allow_destroy: true
# File activerecord/lib/active_record/nested_attributes.rb, line 351
def accepts_nested_attributes_for(*attr_names)
  options = { allow_destroy: false, update_only: false }
  options.update(attr_names.extract_options!)
  options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
  options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank

  attr_names.each do |association_name|
    if reflection = _reflect_on_association(association_name)
      reflection.autosave = true
      define_autosave_validation_callbacks(reflection)

      nested_attributes_options = self.nested_attributes_options.dup
      nested_attributes_options[association_name.to_sym] = options
      self.nested_attributes_options = nested_attributes_options

      type = (reflection.collection? ? :collection : :one_to_one)
      generate_association_writer(association_name, type)
    else
      raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
    end
  end
end