Mar 212009
 

This week at work I was disappointed to find that calling #new on an association collection doesn’t add the new instance to the cached collection:


>> patrick = Poster.first
=> #
>> patrick.posts.size
=> 0

>> new_post = patrick.posts.new(:title => "Another post")
=> #

>> patrick.posts.size
=> 0

If I like the post and decide to save it to the database, my cached posts still doesn’t get updated:


>> new_post.save
=> true

>> patrick.posts.size
=> 0

In order to get the collection up-to-date I have to know that this is a trouble spot, and call #reload on the association:


>> patrick.posts.reload; patrick.posts.size
=> 1

If, instead, I try to push that new instance onto the collection explicitly, it gets saved automatically, which doesn’t seem very intuitive (I *much* prefer to have to explicitly save changes to the database):


>> patrick.posts << Post.new(:title => "pushing onto collection")
Post Create (0.8ms) INSERT INTO "posts" ("created_at", "title", "poster_id", "updated_at", "text") VALUES('2009-03-21 06:14:03', 'pushing onto collection', 1, '2009-03-21 06:14:03', NULL)
=> [#]

The right way to do this seems to be the undocumented #build, which does exactly what I was wanting in the first place (create an unsaved instance and append it to the collection):


>> patrick.posts.size
=> 0

>> patrick.posts.build(:title => 'Built')
=> #

>> patrick.posts.size
=> 1

>> patrick.posts
=> [#
]
>> patrick.posts[0].new_record?
=> true

(Unfortunately, there seems to be a related problem with the association’s #delete/#destroy)


>> patrick.posts.size
=> 2

>> patrick.posts.last
=> #

>> patrick.posts.last.destroy
=> #


>> patrick.posts.size
=> 2
>> patrick.posts.reload; patrick.posts.size
=> 1

  3 Responses to “Rails Association Caching Pitfalls”

  1. I think some of this is caused by the difference between count, length and size (http://blog.hasmanythrough.com/2008/2/27/count-length-size). I believe that length will return your expected value throughout.

    One other thing to consider is that, if you save the patrick object, rather than the new_post object, it will save new_post to the DB and return the correct size from the association.

    But, ultimately, your point is well taken. There is memoization going on here that we need to take into account. Thanks for posting — hopefully it will help me to remember this potential trap :)

  2. eee.c — Well, you’re certainly right about there being a difference between size/length/count.

    I was using #size here for brevity, but it looks like I glossed over the fact that the underlying issue here is what is getting cached. It isn’t the count (#length and #size return the same in my examples), but rather the array of associated elements getting cached.

    For the #new example, despite the instantiated object having the correct poster_id, it doesn’t get added to patrick’s cached collection of posts. So, calling “patrick.save” doesn’t trigger the save of new_post, and a subsequent call to patrick.posts.length (or size) doesn’t represent the new_post we intended to create.

  3. I didn’t realize #build was undocumented; it was what immediately came to mind when I started reading this. I guess I learned about it from books, etc. rather than the Rails API docs.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>