Использовать вложенную модель рельсов для * создания * внешнего объекта и одновременно * редактирования * существующего вложенного объекта?
Использование 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 %>
Работает нормально, когда я создаю нового пользователя с помощью вложенной модели, но не работает, если вложенный пользователь уже существует и имеет идентификатор (в этом случае я бы хотел, чтобы он просто обновил этого пользователя).
Ошибка:
Couldn't find User with ID=7 for Blogger with ID=
Этот SO вопрос касается аналогичной проблемы, и только ответ предполагает, что Rails просто не будет работать таким образом. В ответе предлагается просто передать идентификатор существующего элемента, а не показывать форму для него, что прекрасно работает, за исключением того, что я хотел бы разрешить редактирование атрибутов пользователя, если они есть.
Глубоко вложенные формы Rails, использующие принадлежащие_, не работают?
Предложения? Это не похоже на необычную ситуацию, и кажется, что должно быть решение.
3 ответа
Я использую 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
запись от блоггера 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
Это ошибка, которую вы видели.
Наконец, если ни один из двух других, если ветви были взяты, он проверяет, должен ли он отклонить новую запись, и (если нет) приступает к созданию новой записи. Это тот путь, который следует в случае "создать нового пользователя, если он еще не существует", о котором вы упоминали.
Обходной путь 1 (не рекомендуется): without_protection: true
Первый обходной путь, о котором я подумал - но не рекомендую, - это присвоить атрибуты объекту Blogger с помощью without_protection: true
(Рельсы 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
Пользователь из остальных представленных пользователем параметров.
Я столкнулся с той же ошибкой в рельсах 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 => "thing name" }}}}}}
Попробуйте добавить скрытое поле для идентификатора пользователя во вложенной форме:
<%=user_form.hidden_field :id%>
Вложенное сохранение будет использовать это, чтобы определить, является ли это созданием или обновлением для Пользователя.