我正在使用Rails 3.2.8,遇到了完全相同的问题。
看起来你试图做的事情(将一个已存在的已保存记录分配/更新到一个新的未保存父模型(Blogger
)的一个belongs_to
关联(user
))在Rails 3.2.8(或Rails 2.3.8)中似乎是不可能的,不过我希望你现在已经升级到了3.x版本...除非采用一些变通方法。
我发现了两种变通方法,它们似乎在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
关联(
record = send(association_name)
,其中association_name是
:user
)获取现有的
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
(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
,您需要使用 Edge 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,这样就会从提交的用户参数中构建一个新的
用户。