Использовать вложенную модель рельсов для * создания * внешнего объекта и одновременно * редактирования * существующего вложенного объекта? - PullRequest
19 голосов
/ 14 июня 2011

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

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

Модель:

* * 1010

Контроллер 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 %>

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

Ошибка:

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

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

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

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

Ответы [ 3 ]

48 голосов
/ 22 августа 2012

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

Похоже, что вы пытаетесь сделать (назначить / обновить существующую сохраненную запись дляbelongs_to ассоциация (user) новой несохраненной родительской модели (Blogger) просто невозможна в Rails 3.2.8 (или Rails 2.3.8, если на то пошло, хотя I надеюсь вы уже обновились до 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 от блоггера userassociation (record = send(association_name), где 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] имеет значение true, поэтому он переходит к raise_nested_attributes_record_not_found, что является ошибкой, которую вы видели.

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


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

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

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

Таким образом, он пропускает1-й 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, вам нужно будет либо использовать пограничные 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

Этот подход также предотвращает возникновение ошибки., но делает это немного по-другому.

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

2 голосов
/ 02 августа 2013

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

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

при условии, что params выглядит примерно так ... {: report => {: name => "name",: user_attributes => {: id =>1, {: things_attributes => {"1" => {: name => "имя вещи"}}}}}}

0 голосов
/ 14 июня 2011

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

<%=user_form.hidden_field :id%>

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...