Подтвердить что ты не робот

Использовать вложенную модель rails для * создания * внешнего объекта и одновременно * редактировать * существующий вложенный объект?

Использование Rails 2.3.8

Цель - создать Blogger, а одновременно обновить вложенную модель пользователя (в случае изменения информации и т.д.) ИЛИ создать нового пользователя, если он Пока еще нет.

Модель:

class Blogger < ActiveRecord::Base
  belongs_to :user
  accepts_nested_attributes_for :user
end

Контроллер Blogger:

def new
  @blogger = Blogger.new
  if user = self.get_user_from_session
    @blogger.user = user
  else
    @blogger.build_user
  end
  # get_user_from_session returns existing user 
  # saved in session (if there is one)
end

def create
  @blogger = Blogger.new(params[:blogger])
  # ...
end

форма:

<% form_for(@blogger) do |blogger_form| %>
  <% blogger_form.fields_for :user do |user_form| %>
    <%= user_form.label :first_name %>
    <%= user_form.text_field :first_name %>
    # ... other fields for user
  <% end %>
  # ... other fields for blogger
<% end %>

Хорошо работает, когда я создаю нового пользователя через вложенную модель, но терпит неудачу, если вложенный пользователь уже существует и имеет и ID (в этом случае мне бы хотелось, чтобы он просто обновлял эту пользователь).

Ошибка:

Couldn't find User with ID=7 for Blogger with ID=

Этот вопрос SO относится к аналогичной проблеме, и только ответ предполагает, что Rails просто не будет работать таким образом. Ответ предполагает просто передать идентификатор существующего элемента, а не показывать его форму, что прекрасно работает, за исключением того, что я хочу разрешить изменения атрибутов User, если они есть.

Глубоко вложенные формы Rails с использованием функции принадлежать неработающим?

Предложения? Это не кажется особенно необычной ситуацией, и, похоже, должно быть решение.

4b9b3361

Ответ 1

Я использую Rails 3.2.8 и выполняю ту же самую проблему.

Похоже, что вы пытаетесь сделать (назначить/обновить существующую сохраненную запись в ассоциации belongs_to (user) нового несохраненного родителя модель (Blogger) просто невозможна в Rails 3.2.8 (или Rails 2.3.8, если на то пошло, хотя я надеюсь, что вы уже обновили до 3.x)... не без каких-либо обходных решений.

Я нашел 2 обходных решения, которые, похоже, работают (в Rails 3.2.8). Чтобы понять, почему они работают, вы должны сначала понять код, в котором он поднимал ошибку.

Понимание того, почему ActiveRecord вызывает ошибку...

В моей версии activerecord (3.2.8) код, который обрабатывает назначение вложенных атрибутов для ассоциации belongs_to, можно найти в lib/active_record/nested_attributes.rb:332 и выглядит следующим образом:

def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
  options = self.nested_attributes_options[association_name]
  attributes = attributes.with_indifferent_access

  if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
      (options[:update_only] || record.id.to_s == attributes['id'].to_s)
    assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes)

  elsif attributes['id'].present? && !assignment_opts[:without_protection]
    raise_nested_attributes_record_not_found(association_name, attributes['id'])

  elsif !reject_new_record?(association_name, attributes)
    method = "build_#{association_name}"
    if respond_to?(method)
      send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
    else
      raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
    end
  end
end

В операторе if, если он видит, что вы передали идентификатор пользователя (!attributes['id'].blank?), он пытается получить существующую запись user из ассоциации blogger user (record = send(association_name), где имя_соединения :user).

Но так как это недавно построенный объект Blogger, blogger.user будет изначально иметь значение nil, поэтому он не получит вызов assign_to_or_mark_for_destruction в этой ветке, который обрабатывает обновление существующего record, Это то, что нам нужно обойти (см. Следующий раздел).

Итак, он переходит к 1-й ветки else if, которая снова проверяет наличие идентификатора пользователя (attributes['id'].present?). Он присутствует, поэтому он проверяет следующее условие, которое !assignment_opts[:without_protection].

Поскольку вы инициализируете свой новый объект Blogger с помощью Blogger.new(params[:blogger]) (то есть без передачи as: :role или without_protection: true), он использует assignment_opts по умолчанию {}. !{}[:without_protection] истинно, поэтому он переходит к raise_nested_attributes_record_not_found, который является ошибкой, которую вы видели.

Наконец, если не было сделано ни одной из двух других ветвей if, он проверяет, должен ли он отклонить новую запись, и (если нет) продолжает строить новую запись. Это путь, который следует использовать в "создании совершенно нового пользователя, если он еще не существует", о котором вы говорили.


Обходной путь 1 (не рекомендуется): without_protection: true

Первое обходное решение, о котором я думал, но не рекомендую, - это назначить атрибуты объекту Blogger с помощью without_protection: true (Rails 3.2.8).

Blogger.new(params[:blogger], without_protection: true)

Таким образом, он пропускает первый elsif и переходит к последнему elsif, который создает нового пользователя со всеми атрибутами из параметров, включая :id. На самом деле, я не знаю, приведет ли это к обновлению существующей записи пользователя, как вы этого хотели (вероятно, не очень-то не проверили эту опцию), но, по крайней мере, она избегает ошибки...:)

Обходной путь 2 (рекомендуется): установите self.user в user_attributes=

Но обходной путь, который я бы рекомендовал больше, - это фактически инициализировать/установить ассоциацию user из параметра: id, чтобы использовать первую ветвь if, и она обновляет существующую запись в памяти, как вы хотите...

  accepts_nested_attributes_for :user
  def user_attributes=(attributes)
    if attributes['id'].present?
      self.user = User.find(attributes['id'])
    end
    super
  end

Чтобы иметь возможность переопределять вложенные атрибуты, подобные этому, и вызывать super, вам нужно либо использовать edge Rails, либо включить патч обезьяны, который я разместил в https://github.com/rails/rails/pull/2945. Кроме того, вы можете просто вызвать assign_nested_attributes_for_one_to_one_association(:user, attributes) непосредственно из вашего установщика user_attributes= вместо вызова super.


Если вы хотите, чтобы он всегда создавал новую запись пользователя и не обновлял существующего пользователя...

В моем случае я решил, что не хочу, чтобы люди могли обновлять существующие пользовательские записи из этой формы, поэтому в итоге я использовал небольшое изменение обходного пути выше:

  accepts_nested_attributes_for :user
  def user_attributes=(attributes)
    if user.nil? && attributes['id'].present?
      attributes.delete('id')
    end
    super
  end

Этот подход также предотвращает возникновение ошибки, но делает это несколько иначе.

Если в параметрах передан id, вместо того чтобы использовать его для инициализации ассоциации user, я просто удаляю идентификатор переданного кода, чтобы он возвращался к созданию пользователя new из остальной части представленные параметры пользователя.

Ответ 2

Я столкнулся с той же ошибкой в ​​рельсах 3.2. Ошибка возникла при использовании вложенной формы для создания нового объекта с атрибутом, принадлежащим отношению к существующему объекту. Подход Тайлера Рика не помог мне. То, что я нашел для работы, это установить отношения после инициализации объекта и затем установить атрибуты объектов. Пример этого следующий:

@report = Report.new()
@report.user = current_user
@report.attributes = params[:report] 

Предполагая, что параметры выглядят как... {: report = > {: name = > "name",: user_attributes = > {: id = > 1, {: things_attributes = > { "1" = > {: name = > "thing name" }}}}} }

Ответ 3

Попробуйте добавить скрытое поле для идентификатора пользователя в вложенной форме:

<%=user_form.hidden_field :id%>

Вложенное сохранение будет использовать это, чтобы определить, является ли это созданием или обновлением для пользователя.