3

User can have many posts:

class User < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end

Why the following sequence doesn't update the first post?

$ 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"?
4

1 回答 1

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观察如何实现批发替换has_many

# 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

工作的核心在于replace_records

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传入参数的 as 是返回的内容,这似乎反映了更改:

=> [#<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">]>
>

注意这里的返回略有不同;赋值的返回值是你传入的数组对象;这是一个ActiveRecord::Associations::CollectionProxy. 该reader函数在这里调用

# 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

然后,这会基于 has_many 关系创建集合代理,其值是根据我们在分配选项时所知道的内容填充的。这个答案中唯一未被发现的部分是为什么结果对象被清除了脏值——我已经阅读了一些代码,并认为使用调试器最容易回答,但我没有心情为了。:) 但很明显,它要么是从缓存中加载,要么是传入的对象正在丢弃它们的更改。

无论哪种方式,如果您希望更改出现在目标对象中,您应该先保存它——仅仅分配集合是不够的,就好像它已经是一个成员一样,它不会被触及。


更新:有趣的是,这只是因为我们使用Post.find来获取post1; 如果我们改为说post1 = (user.posts << Post.create(description: 'p1')),最后观察到的集合user.posts实际上有脏对象。

这首先揭示了它是如何形成的。观看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

> 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
>

最初让我感到困惑的部分是没有脏数据的对象来自哪里;没有发生 SQL,甚至没有缓存命中,所以它必须来自某个地方。我曾认为它要么是其他一些缓存源,要么是明确地获取给定的对象并清理它们。

上面清楚地表明缓存实际上是Post我们在插入时创建的。为了 100% 确定,让我们看看返回的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其中不反映更改的对象实际上是我们最初添加到集合时创建的相同对象;这解释了缓存数据的来源。然后我们置换一个副本,它不会反映在收集后分配中。

于 2013-06-17T11:24:27.157 回答