【问题标题】:Use rails nested model to *create* outer object and simultaneously *edit* existing nested object?使用rails嵌套模型*创建*外部对象并同时*编辑*现有嵌套对象?
【发布时间】:2011-06-14 15:33:21
【问题描述】:

使用 Rails 2.3.8

目标是创建一个 Blogger,同时更新嵌套的用户模型(以防信息发生变化等),或者创建一个全新的用户(如果没有) '还不存在。

型号:

class Blogger < ActiveRecord::Base
  belongs_to :user
  accepts_nested_attributes_for :user
end

博主控制器:

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 question 处理类似的问题,只有答案表明 Rails 根本不会那样工作。答案建议简单地传递现有项目的 ID,而不是显示它的表单——这很好,除非我想允许对 User 属性进行编辑(如果有的话)。

Deeply nested Rails forms using belong_to not working?

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

【问题讨论】:

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


    【解决方案1】:

    我使用的是 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 语句中,如果它看到您传递了一个用户ID (!attributes['id'].blank?),它会尝试从博主的user 关联中获取现有的user 记录(record = send(association_name) 其中关联名称为@ 987654335@)。

    但由于这是一个新建的 Blogger 对象,blogger.user 最初将是 nil,因此它不会到达该分支中处理更新现有 recordassign_to_or_mark_for_destruction 调用.这是我们需要解决的问题(请参阅下一节)。

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

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

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


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

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

    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
    

    为了能够像这样覆盖嵌套属性访问器并调用super,您需要使用边缘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
    

    这种方法也可以防止错误发生,但做法略有不同。

    如果在 params 中传递了一个 id,而不是使用它来初始化 user 关联,我只是删除传入的 id,以便它将回退到从其余部分构建一个 new 用户提交用户参数。

    【讨论】:

    • 您的“解决方法 2”很好,因为它让 Rails 不碍事,所以我们可以进入我们的“创建”块代码,在那里我们可以处理嵌套模型。我发现甚至在进入创建块之前就发生了 OPs 错误 - 令人抓狂。在我的情况下,如果它存在,请更新父模型的belongs_to id-field并用'.except(:nested_model)'去除嵌套参数 - 或创建为新实例,在这种情况下保存很好嵌套的参数。
    • 这个解决方案很棒,它帮助我解决了我的问题。但是,我想知道如何实现类似的方法来处理多对多关系......如果没有覆盖,我总是会到达父对象(我第一次创建)无法创建嵌套关系的地步它的一些孩子已经有一个以前的ID(因为它是多对多的,他们可以有“许多父母”),在关联表上找不到404。如果我用正确的数组查找方法覆盖,我会得到一个 DB 错误(在我的例子中是 PG),因为在关联表中 parent_id 是 nil。
    • 最新版本的 Rails 中添加了这个吗?
    • @StefanoMondino 我认为你可以做类似github.com/rails/rails/issues/7256#issuecomment-249735086
    • 对于迟到的访问者,解决方法 #1 不再是一个选项,并已使用 commit 2d7ae1b08ee2a10b12cbfeef3a6cc6da55b57df6 (Rails 4.0.0) 删除。
    【解决方案2】:

    我在 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 => "thing name" }}}}} }

    【讨论】:

      【解决方案3】:

      尝试在嵌套表单中为用户的 id 添加隐藏字段:

      <%=user_form.hidden_field :id%>
      

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

      【讨论】:

      • Rails 实际上已经这样做了,基本上。如果 User 对象已经存在,它会添加一个隐藏的 blogger[user_attributes][id] 表单字段(如果它是 new 用户则不存在)。正如我原始笔记中的错误消息,Rails 知道嵌套模型的 ID。
      • 是的,传递的嵌套对象的ID绝对不是问题。在我粘贴的原始错误中,它知道嵌套对象(用户)的 ID 是 7,而外部对象(Blogger)还没有 ID(因为它是全新的)。在那种情况下,它显然会吓坏。
      猜你喜欢
      • 2022-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-25
      • 1970-01-01
      • 1970-01-01
      • 2019-04-13
      相关资源
      最近更新 更多