问题
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"?
回答1:
You're mixing up saving the post object with saving the association from posts to users.
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
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.
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
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
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.
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">]>
>
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
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.
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.
This unveils how it came into existence in the first place. Watch the object_id
s:
>
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
>
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.
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
>
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.
来源:https://stackoverflow.com/questions/17142290/rails-why-collection-doesnt-update-records-with-existing-id