跳到內文 跳到搜尋

Active Record Nested Attributes

嵌套屬性讓您可以透過父類別儲存關聯記錄中的屬性。預設關閉嵌套屬性更新,您可以使用 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

請注意,使用 accepts_nested_attributes_for 的所有關聯都會自動啟用 :autosave 選項。

一對一

考慮一個有頭像的 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 金鑰的每個雜湊,將會實例化一個新記錄,除非雜湊還包含 _destroy 金鑰並評估為 true

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 程序,來不聲不響地忽視任何新的記錄雜湊,前提是它們未通過您的判斷標準。例如,前一個範例可以改寫為

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

驗證是否存在父模型

預設情況下,belongs_to 關聯會驗證父模型是否存在。您可以指定 optional: true 來停用此行為。例如,用在有條件地驗證是否存在父模型時

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

請注意,如果您未指定 :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 建立巢狀屬性的表單元素。

Integration 測試參數應反映表單的結構。例如

post members_path, params: {
  member: {
    name: 'joe',
    posts_attributes: {
      '0' => { title: 'Foo' },
      '1' => { title: 'Bar' }
    }
  }
}
方法
A

常數

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

執行個體公開的方法

accepts_nested_attributes_for(*attr_names)

定義指定關聯的屬性 writer。

支援的選項

:allow_destroy

如果是 true,就毀棄屬性雜湊中具有 _destroy 金鑰,且評估後等於 true(例如 1、‘1’、true 或 ‘true’)的值的任何成員。這個選項預設為 false。

:reject_if

讓您可以指定一個 Proc 或一個指向檢查某個屬性雜湊是否應建置為某個記錄的方法的 Symbol。將雜湊傳遞到提供的 Proc 或方法,它應傳回 truefalse。如果未指定 :reject_if,將為所有未具有評估後等於 true _destroy 值的屬性雜湊建置記錄。傳遞 :all_blank(而不是 Proc)會建立一個 Proc,拒絕所有屬性為空白的記錄(排除任何 _destroy 值)。

:limit

讓您可以指定使用巢狀屬性處理的關聯記錄的最大數目。限制也可以指定為 Proc 或一個指向應傳回數字的方法的 Symbol。如果巢狀屬性陣列的大小超過指定的限制,則引發 NestedAttributes::TooManyRecords 例外。如果省略,則可以處理任何數量的關聯。請注意,:limit 選項僅適用於一對多關聯。

:update_only

對於一對一關聯,此選項允許您在關聯記錄已存在時,指定嵌套屬性將如何使用。一般來說,現有的記錄可能會使用新的屬性值集進行更新,或者由包含這些值的新記錄完全替換。預設情況下,:update_only 選項為 false,且嵌套的屬性僅在包含記錄的 :id 值時,才會用於更新現有記錄。否則,將實例化一個新的記錄,並用於替換現有的記錄。但是,如果 :update_only 選項為 true,則嵌套的屬性將用於更新記錄的屬性,而不論 :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