Spec-Zone.ru › Ruby on Rails 8.1

модуль ActiveRecord::NestedAttributes::ClassMethods

Вложенные атрибуты 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

Обратите внимание: для каждой ассоциации, к которой применяется accepts_nested_attributes_for, параметр :autosave включается автоматически.

Один к одному

Рассмотрим модель Member, у которой есть один Avatar:

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'

Чтобы обновить текущий аватар без указания идентификатора, необходимо добавить параметр :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

Обратите внимание: модель не будет уничтожена, пока не будет сохранена родительская модель.

Также обратите внимание: модель не будет уничтожена, если в обновленном хеше не указан ее идентификатор.

Один ко многим

Рассмотрим участника, у которого есть несколько публикаций:

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'

Также можно задать Proc :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'

Однако это работает, если родительская модель тоже обновляется. Например, если нужно создать member с именем joe и одновременно обновить 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' }
    }
  }
}

Константы

REJECT_ALL_BLANK_PROC

Открытые методы экземпляра

accepts_nested_attributes_for (*attr_names) Показать исходный код
# 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

Определяет метод записи атрибутов для указанных ассоциаций.

Поддерживаемые параметры:

:allow_destroy

Если значение равно true, уничтожает записи из хеша атрибутов, у которых есть ключ _destroy со значением, которое при вычислении дает true (например, 1, ‘1’, true или ‘true’). По умолчанию этот параметр имеет значение false.

:reject_if

Позволяет указать Proc или Symbol, ссылающийся на метод, который проверяет, следует ли создавать запись для заданного хеша атрибутов. Хеш передается указанному Proc или методу, который должен вернуть true либо false. Если :reject_if не указан, запись будет создана для всех хешей атрибутов, в которых значение _destroy не дает true при вычислении. Если вместо Proc передать :all_blank, будет создан 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

© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API