用户可以有很多帖子:
class User < ActiveRecord::Base
has_many :posts
accepts_nested_attributes_for :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
为什么以下序列不会更新第一篇文章?
$ rails c
> user = User.create(name: 'Misha')
=> #<User id: 7, name: "Misha", ... >
> user.posts << Post.create(description: 'hello')
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 9, description: "hello", user_id: 7, ... >]>
> post1 = Post.find(9)
> post1.assign_attributes(description: 'world')
> post1
=> #<Post id: 9, description: "world", user_id: 7, ... >
> post2 = Post.new(description: 'new post')
> user.posts = [post1, post2]
> user.posts.second.description
=> "new post" # As expected
> user.posts.first.description
=> "hello" # Why not "world"?
答案 0 :(得分:4)
您正在混合保存帖子对象,并保存帖子与用户之间的关联。
像@zeantsoi所说,assign_attributes
从不保存它 - 并且查看执行的SQL,collection=
也没有保存任何内容。
> user.posts = [post1, post2]
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO "posts" ("created_at", "description", "updated_at", "user_id") VALUES (?, ?, ?, ?) [["created_at", Mon, 17 Jun 2013 10:48:13 UTC +00:00], ["des
cription", "p2"], ["updated_at", Mon, 17 Jun 2013 10:48:13 UTC +00:00], ["user_id", 2]]
(22.8ms) commit transaction
=> [#<Post id: 3, description: "p1 modified", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">, #<Post id: 4, description: "p2", user_id:
2, created_at: "2013-06-17 10:48:13", updated_at: "2013-06-17 10:48:13">]
>
只插入 post2
因为必须设置关系才能插入;如果无法唯一地识别User
,则Post
对象无法知道特定的Post
是否属于它。
查看构建CollectionAssociation
的{{1}}的{{1}}来源observe how wholesale replacement is implemented:
has_many
工作的核心是replace_records
:
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
def replace(other_array)
other_array.each { |val| raise_on_type_mismatch!(val) }
original_target = load_target.dup
if owner.new_record?
replace_records(other_array, original_target)
else
transaction { replace_records(other_array, original_target) }
end
end
换句话说,它删除不在目标列表中的项目,然后添加不在新列表中的项目;结果是在收集分配期间根本没有触及目标和新列表(def replace_records(new_target, original_target)
delete(target - new_target)
unless concat(new_target - target)
@target = original_target
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
"new records could not be saved."
end
target
end
)中的任何项目。
根据上面的代码,传递给参数的post1
是返回的内容,似乎以反映更改:
target
但是在重新访问集合时,更改没有反映出来:
=> [#<Post id: 3, description: "p1 modified", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">, #<Post id: 4, description: "p2", user_id:
2, created_at: "2013-06-17 10:48:13", updated_at: "2013-06-17 10:48:13">]
请注意,这里的回报略有不同;赋值的返回值是您传入的数组对象;这是> post1
=> #<Post id: 3, description: "p1 modified", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">
> user.posts
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 3, description: "p1", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">, #<Pos
t id: 4, description: "p2", user_id: 2, created_at: "2013-06-17 10:48:13", updated_at: "2013-06-17 10:48:13">]>
>
。 reader
function is called here:
ActiveRecord::Associations::CollectionProxy
然后,这将基于has_many关系创建集合代理,其值从我们分配选项时所知道的内容中填充。这个答案的唯一未被发现的部分就是生成的对象是清洗脏值 - 我已经做了一些代码阅读,并计算这将是最简单的一个调试器,这我没有心情回答对于。 :)但很明显它是从缓存中加载,或者传入的对象正在丢弃它们的更改。
无论哪种方式,如果您希望更改出现在目标对象中,您应该首先保存它 - 仅仅指定集合不够好,就好像它已经是成员一样,它不会被触及。
更新:有趣的是,请注意这只是因为我们使用# Implements the reader method, e.g. foo.items for Foo.has_many :items
def reader(force_reload = false)
if force_reload
klass.uncached { reload }
elsif stale_target?
reload
end
@proxy ||= CollectionProxy.new(klass, self)
end
来获取Post.find
;如果我们改为说post1
,那么最后在post1 = (user.posts << Post.create(description: 'p1'))
中观察到的集合实际上有脏对象。
这首先揭示了它是如何形成的。观看user.posts
s:
object_id
请注意,集合代理中返回的对象与我们创建的对象相同。如果我们重新>
u = User.create; p1 = (u.posts << Post.create(description: 'p1'))[0]; p1.assign_attributes(description: 'p1 mod'); p2 = Post.new(description: 'p2'); u.posts = [p1, p2]; u.posts
...
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 21, description: "p1 mod", user_id: 10, created_at: "2013-06-17 11:43:30", updated_at: "2013-06-17 11:43:30">, #<Post id: 22, description: "p2", user_id: 10, created_at: "2013-06-17 11:43:30", updated_at: "2013-06-17 11:43:30">]>
> _[0].object_id
=> 70160940234280
> p1.object_id
=> 70160940234280
>
它:
find
让我困惑的原始问题的一部分是没有脏数据的对象来自哪里;没有SQL发生,甚至没有缓存命中,所以它必须来自某个地方。我原本以为它是其他缓存源,或者是明确地获取给定的对象并清理它们。
以上清楚地表明缓存实际上是我们在插入时创建的> u = User.create; u.posts << Post.create(description: 'p1'); p1 = Post.find(u.posts.first.id); p1.assign_attributes(description: 'p1 mod'); p2 = Post.new(description: 'p2'); u.posts = [p1, p2]; u.posts
...=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 23, description: "p1", user_id: 11, created_at: "2013-06-17 11:43:47", updated_at: "2013-06-17 11:43:47">, #<Post id: 24, description: "p2", user_id: 11, created_at: "2013-06-17 11:43:47", updated_at: "2013-06-17 11:43:47">]>
> _[0].object_id
=> 70264436302820
> p1.object_id
=> 70264441827000
>
。要100%确定,让我们看看返回的Post
是否与创建的Post
相同:
> u = User.create; p0 = (u.posts << Post.create(description: 'p1'))[0]; p1 = Post.find(u.posts.first.id); p1.assign_attributes(description: 'p1 mod'); p2 = Post.new(description: 'p2'); u.posts = [p1, p2]; u.posts
...
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 27, description: "p1", user_id: 13, created_at: "2013-06-17 12:01:05", updated_at: "2013-06-17 12:01:05">, #<Post id: 28, description: "p2", user_id: 13, created_at: "2013-06-17 12:01:07", updated_at: "2013-06-17 12:01:07">]>
> _[0].object_id
=> 70306779571100
> p0.object_id
=> 70306779571100
> p1.object_id
=> 70306779727620
>
因此,CollectionProxy
中不反映变化的对象实际上是我们在首先追加到集合时创建的对象;这解释了缓存数据的来源。然后我们置换副本,这不会在收集后分配后反映出来。