我使用的是 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,因此它不会到达该分支中处理更新现有 record 的 assign_to_or_mark_for_destruction 调用.这是我们需要解决的问题(请参阅下一节)。
所以它移动到第一个else if 分支,再次检查是否存在用户 ID (attributes['id'].present?)。它存在,因此它检查下一个条件,即!assignment_opts[:without_protection]。
由于您使用Blogger.new(params[:blogger])(即不传递as: :role 或without_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 用户提交用户参数。