Rails:为什么“collection="不更新具有现有 ID 的记录? [英] Rails: Why “collection=” doesn't update records with existing id?

查看:32
本文介绍了Rails:为什么“collection="不更新具有现有 ID 的记录?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

用户可以有很多帖子:

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

推荐答案

您将保存帖子对象与保存从帖子到用户的关联混为一谈.

You're mixing up saving the post object with saving the association from posts to users.

就像@zeantsoi 所说的那样,assign_attributes 永远不会保存它——查看执行的 SQL,collection= 也不会保存任何东西.

Like @zeantsoi said, assign_attributes never saves it -- and looking at the executed SQL, collection= doesn't save anything either.

> 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 被插入只是因为它必须是为了设置关系;如果无法唯一标识 Post,则 User 对象无法知道特定的 Post 属于它.

post2 is inserted only because it has to be in order for a relationship to be set; the User object can't know that a particular Post belongs to it if there's no way to identify the Post uniquely.

查看 CollectionAssociation 的源代码,has_many 是基于它构建的,观察批量替换是如何实现的:

Looking at the source for CollectionAssociation, upon which has_many is built, observe how wholesale replacement is implemented:

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

The core of the work is in 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) 中的任何项目.

In other words, it deletes items not in the target list, then adds items not in the new list; the result is that any item that was in both target and new list (post1) isn't touched at all during collection assignment.

根据上面的代码,传入参数的 target 是返回的内容,似乎反映了变化:

Per the above code, the target as passed in to the argument is what's returned, which seems to reflect the change:

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

但是在重新访问集合时,更改不会反映出来:

But upon reaccessing the collection, the change isn't reflected:

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

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

Note that the return here is slightly different; the return value from the assignment was the array object you passed in; this here is an ActiveRecord::Associations::CollectionProxy. The reader function is called here:

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

This, then, creates the collection proxy based on the has_many relation, whose values are filled in from what we knew when we assigned the options. The only undiscovered part of this answer is why the resulting object is cleaned of dirty values -- I've done a bit of code reading, and figure it'll be easiest to answer with a debugger, which I'm not in the mood for. :) But it's clear that it's loading either from cache, or the objects passed in are having their changes discarded.

不管怎样,如果你想让改变出现在目标对象中,你应该先保存它——仅仅分配集合是不够的,好像它已经是一个成员,它不会被触及.

Either way, if you want the change to appear in the target object, you should save it first -- merely assigning the collection isn't good enough, as if it was already a member, it won't be touched.

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

Update: it's interesting to note this happens only because we use Post.find to obtain post1; if we instead say post1 = (user.posts << Post.create(description: 'p1')), the collection as observed in user.posts at the end actually has the dirty object.

这首先揭示了它是如何产生的.观看 object_ids:

This unveils how it came into existence in the first place. Watch the object_ids:

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

注意集合代理中返回的对象与我们创建的对象相同.如果我们重新找到它:

Note the returned object in the collection proxy is the same object as that we created. If we re-find it:

> 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 发生,甚至没有命中缓存,所以它必须来自某个地方.我原以为它是其他一些缓存源,或者它正在明确获取给定的对象并清理它们.

The part of the original question that confused me was where the object without the dirty data was coming from; no SQL occurred, not even a cache hit, so it had to come from somewhere. I had supposed it was either some other cache source, or it was explicitly taking the objects given and cleaning them.

上面清楚地表明缓存实际上是我们插入时创建的Post.为了 100% 确定,让我们看看返回的 Post 是否与创建的相同:

The above makes it clear that the cache is in fact the Post we created when inserting it. To be 100% sure, let's see if the returned Post is the same as the created one:

> 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 中不反映变化的对象实际上是我们在追加到集合时创建的对象;这解释了缓存数据的来源.然后我们置换一个副本,它不会在集合分配后得到反映.

So the object in the CollectionProxy which doesn't reflect the change is in fact the same object we created when appending to the collection in the first place; that explains the source of the cached data. We then permute a copy, which doesn't get reflected post-collection-assignment.

这篇关于Rails:为什么“collection="不更新具有现有 ID 的记录?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆