使用rails嵌套模型来创建*外部对象并同时*编辑*现有的嵌套对象?
使用Rails 2.3.8
目标是创建一个Blogger,同时更新嵌套的User模型(如果信息已更改等),或者创建一个全新的用户(如果它尚不存在)。
模型:
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
形成:
# ... other fields for user # ... other fields for blogger
当我通过嵌套模型创建新用户时工作正常,但如果嵌套用户已经存在并且具有和ID(在这种情况下我希望它只是更新该用户),则会失败。
错误:
Couldn't find User with ID=7 for Blogger with ID=
这个SO问题处理类似的问题,只有答案表明Rails根本不会那样工作。 答案建议只是传递现有项目的ID而不是显示它的forms – 这样可以正常工作,除非我想允许编辑User属性(如果有的话)。
使用belongs_to深度嵌套的Rails表单不起作用?
建议? 这似乎不是一个特别罕见的情况,似乎必须有一个解决方案。
我正在使用Rails 3.2.8并遇到完全相同的问题。
看来你正在尝试做什么(将现有保存的记录分配/更新到新未保存的父模型( Blogger
)的belongs_to
关联( user
),这在Rails 3.2.8(或Rails 2.3.8,for这件事,虽然我希望你现在已经升级到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
语句中,如果它看到你传递了一个用户ID( !attributes['id'].blank?
),它会尝试从博客的user
关联中获取现有user
记录( record = send(association_name)
其中association_name是:user
)。
但由于这是一个新构建的Blogger
对象,因此blogger.user最初将为nil
,因此它不会进入处理更新现有record
分支中的assign_to_or_mark_for_destruction
调用。 这是我们需要解决的问题(参见下一节)。
所以它转移到第一个else if
分支,它再次检查是否存在用户ID( attributes['id'].present?
)。 它存在,因此它检查下一个条件,即!assignment_opts[:without_protection]
。
由于您使用Blogger.new(params[:blogger])
初始化新的Blogger对象(即,不传递as: :role
或without_protection: true
),因此它使用默认的assignment_opts
{}
。 !{}[:without_protection]
为true,因此会进入raise_nested_attributes_record_not_found
,这是您看到的错误。
最后,如果其他2个if分支都没有被采用,它会检查是否应该拒绝新记录,并且(如果没有)继续构建新记录。 这是你在提到的“创建一个全新的用户,如果它还不存在”的情况下所遵循的路径。
解决方法1(不推荐): without_protection: true
我想到的第一个解决方法 – 但不建议 – 是使用without_protection: true
将属性分配给Blogger对象without_protection: true
(Rails 3.2.8)。
Blogger.new(params[:blogger], without_protection: true)
这样它就会跳过第一个elsif
并转到最后一个elsif
,它会建立一个具有params所有属性的新用户, 包括 :id
。 实际上,我不知道是否会导致它更新现有的用户记录,就像你想要的那样(可能没有真正测试那个选项),但至少它避免了错误… 🙂
解决方法2(推荐):在user_attributes=
设置self.user
但我推荐的解决方法是实际初始化/设置来自:id param的user
关联,以便使用第一个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 或者,您可以直接从user_attributes=
setter调用assign_nested_attributes_for_one_to_one_association(:user, attributes)
,而不是调用super
。
如果你想让它总是创建一个新的用户记录而不是更新现有的用户……
在我的情况下,我最终决定我不希望人们能够从这个表单更新现有的用户记录,所以我最终使用了上面的解决方法的略微变化:
accepts_nested_attributes_for :user def user_attributes=(attributes) if user.nil? && attributes['id'].present? attributes.delete('id') end super end
这种方法也可以防止错误发生,但这样做有点不同。
如果在params中传递了id,而不是使用它来初始化user
关联,我只是删除传入的id,以便它将回退到从其他提交的用户参数构建new
用户。
我在rails 3.2中遇到了同样的错误。 使用嵌套表单创建具有现有对象的属于关系的新对象时发生错误。 Tyler Rick的方法对我不起作用。 我发现工作的是在初始化对象之后设置关系,然后设置对象属性。 这方面的一个例子如下……
@report = Report.new() @report.user = current_user @report.attributes = params[:report]
假设params看起来像… {:report => {:name =>“name”,:user_attributes => {:id => 1,{:things_attributes => {“1”=> {:name =>“事物名称“}}}}}}
尝试在嵌套表单中为用户的id添加隐藏字段:
<%=user_form.hidden_field :id%>
嵌套保存将使用它来确定它是用户的创建还是更新。