使用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不会那样工作。答案是建议简单地传递现有项目的ID,而不是显示其形式-效果很好,除非我想允许对User属性(如果有)进行编辑。 深度嵌套的Rails表单使用before_to无法正常工作? 有什么建议吗?这似乎不是特别罕见的情况,而且似乎必须有解决方案。     
已邀请:
我正在使用Rails 3.2.8,并遇到完全相同的问题。 似乎您想要做的事情(将现有保存的记录分配/更新为新的未保存父模型(
Blogger
)的
belongs_to
关联(
user
)在Rails 3.2.8(或Rails 2.3.8中,没关系,尽管我希望您现在已经升级到3.x)...并非没有一些解决方法。 我发现2个变通办法似乎有效(在Rails 3.2.8中)。要了解它们为什么起作用,您应该首先了解引起错误的代码。 了解为什么ActiveRecord引发错误... 在我的activerecord版本(3.2.8)中,可以在
lib/active_record/nested_attributes.rb:332
中找到处理为
belongs_to
关联分配嵌套属性的代码,如下所示:
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
关联(
record = send(association_name)
,association_name为
:user
)中获取现有的
user
记录。 但是,由于这是一个新建的
Blogger
对象,因此blogger.user最初将为
nil
,因此它将无法处理处理现有ѭ19的分支中的
assign_to_or_mark_for_destruction
调用。这就是我们需要解决的问题(请参阅下一节)。 因此,它移至第一个“ 20”分支,该分支再次检查是否存在用户标识(“ 21”)。它存在,因此它将检查下一个条件
!assignment_opts[:without_protection]
。 由于您要使用
Blogger.new(params[:blogger])
初始化新的Blogger对象(即,不传递
as: :role
without_protection: true
),因此它将使用默认的
assignment_opts
{}
!{}[:without_protection]
是正确的,因此继续进行
raise_nested_attributes_record_not_found
,这是您看到的错误。 最后,如果其他两个都没有分支,它将检查是否应拒绝新记录,并(如果不是)则继续建立新记录。这是您提到的“如果还不存在,则创建新用户”案例中遵循的路径。 解决方法1(不推荐):
without_protection: true
我想到的第一个解决方法-但不建议使用-是使用
without_protection: true
将属性分配给Blogger对象(Rails 3.2.8)。
Blogger.new(params[:blogger], without_protection: true)
这样,它将跳过第一个
elsif
并转到最后一个
elsif
,后者将使用参数中的所有属性(包括
:id
)建立一个新用户。实际上,我不知道这是否会导致它像您想要的那样更新现有的用户记录(可能不是,还没有真正测试过该选项),但是至少它避免了错误... :) 解决方法2(建议):在
user_attributes=
中设置
self.user
但是,我建议的解决方法是,实际上是从:id参数初始化/设置
user
关联,以便使用第一个
if
分支,并根据需要更新内存中的现有记录...
  accepts_nested_attributes_for :user
  def user_attributes=(attributes)
    if attributes[\'id\'].present?
      self.user = User.find(attributes[\'id\'])
    end
    super
  end
为了能够像这样覆盖嵌套的属性访问器并调用ѭ41,您需要使用Edge Rails或包含我在https://github.com/rails/rails/pull发布的猴子补丁/ 2945。另外,您也可以直接从
user_attributes=
设定器中拨打
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
这种方法还可以防止错误的发生,但是却有所不同。 如果在参数中传递了一个id,而不是使用它来初始化
user
关联,我只是删除传入的id,这样它就可以从其余提交的用户参数中退回到构建一个
new
用户。     
        我在Rails 3.2中遇到了相同的错误。使用嵌套表单为现有对象创建一个具有属于关系的新对象时发生错误。泰勒·里克的方法对我不起作用。我发现要工作的是在对象初始化之后设置关系,然后设置对象属性。一个例子如下...
@report = Report.new()
@report.user = current_user
@report.attributes = params[:report] 
假设参数看起来像... {:report => {:name => \“ name \”,:user_attributes => {:id => 1,{:things_attributes => {\“ 1 \” => {:name => \“事物名称\ “}}}}}}     
        尝试以嵌套形式为用户的ID添加一个隐藏字段:
<%=user_form.hidden_field :id%>
嵌套的保存将使用它来确定它是用户的创建还是更新。     

要回复问题请先登录注册