使用rails嵌套模型来创建*外部对象并同时*编辑*现有的嵌套对象?

时间:2011-06-14 15:33:21

标签: ruby-on-rails activerecord nested-forms nested-attributes

使用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和ID,则会失败(在这种情况下,我希望它只是更新它用户)。

错误:

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

这个SO问题涉及类似问题,只有答案表明Rails根本不会那样工作。答案建议只是传递现有项目的ID而不是显示它的表单 - 这样可以正常工作,除非我想允许编辑User属性(如果有的话)。

Deeply nested Rails forms using belong_to not working?

连连呢?这似乎不是一个特别罕见的情况,似乎必须有一个解决方案。

3 个答案:

答案 0 :(得分:48)

我正在使用Rails 3.2.8并遇到完全相同的问题。

您正在尝试执行的操作(将现有的已保存记录分配/更新为新未保存<的belongs_to关联(user) / strong>父模型(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语句中,如果它看到您传递了用户ID(!attributes['id'].blank?),则会尝试从博主的user关联中获取现有的user记录(record = send(association_name)其中association_name为:user)。

但由于这是一个新构建的Blogger对象,blogger.user最初将是nil,所以它不会进入处理该分支的assign_to_or_mark_for_destruction调用更新现有的record。这是我们需要解决的问题(参见下一节)。

因此它转到第一个else if分支,再次检查是否存在用户ID(attributes['id'].present?)。它存在,因此它检查下一个条件,即!assignment_opts[:without_protection]

由于您使用Blogger.new(params[:blogger])初始化新Blogger对象(即未传递as: :rolewithout_protection: true),因此它使用assignment_opts的默认{} }。 !{}[:without_protection]为真,因此会转到raise_nested_attributes_record_not_found,这是您看到的错误。

最后,如果其他2个分支都没有被采用,它会检查是否应该拒绝新记录,并且(如果没有)继续构建新记录。这是它在“创建一个全新用户(如果它还不存在的话)”中所遵循的路径。


解决方法1(不推荐):without_protection: true

我想到的第一个解决方法 - 但不建议 - 是使用without_protection: true(Rails 3.2.8)将属性分配给Blogger对象。

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

这样它会跳过第一个elsif并转到最后一个elsif,它会使用参数中的所有属性构建一个新用户,包括 {{1 }}。实际上,我不知道是否会导致它更新现有的用户记录,就像你想要的那样(可能没有真正测试过那个选项),但至少它避免了错误......:)

解决方法2(推荐):在:id

中设置self.user

但我推荐的解决方法是实际初始化/设置{id} param的user_attributes=关联,以便使用第一个user分支并且更新< / em>你想要的内存中的现有记录......

if

为了能够覆盖这样的嵌套属性访问器并调用 accepts_nested_attributes_for :user def user_attributes=(attributes) if attributes['id'].present? self.user = User.find(attributes['id']) end super end ,您需要使用边缘Rails或包含我在https://github.com/rails/rails/pull/2945发布的猴子补丁。或者,您只需直接通过super setter致电assign_nested_attributes_for_one_to_one_association(:user, attributes),而不是致电user_attributes=


如果您想让它始终创建新用户记录而更新现有用户......

在我的情况下,我最终决定没有希望人们能够从此表单更新现有用户记录,因此我最终使用了上述变通方法的略微变化:

super

这种方法也可以防止错误发生,但确实有所不同。

如果在params中传递了id,而不是使用它初始化 accepts_nested_attributes_for :user def user_attributes=(attributes) if user.nil? && attributes['id'].present? attributes.delete('id') end super end 关联,我只是删除传入的ID,以便它将回退到构建user用户其余提交的用户参数。

答案 1 :(得分:2)

我在rails 3.2中遇到了同样的错误。使用嵌套表单创建具有现有对象的属于关系的新对象时发生错误。 Tyler Rick的方法对我不起作用。我发现工作的是在初始化对象之后设置关系,然后设置对象属性。这方面的一个例子如下......

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

假设params看起来像...... {:report =&gt; {:name =&gt; “name”,:user_attributes =&gt; {:id =&gt; 1,{:things_attributes =&gt; {“1”=&gt; {:name =&gt; “事物名称”}}}}}}

答案 2 :(得分:0)

尝试以嵌套形式为用户ID添加隐藏字段:

<%=user_form.hidden_field :id%>

嵌套保存将使用它来确定它是用户的创建还是更新。