I'm trying to cache an ActiveRecord with its association. The problem is there's a database query when accessing the associations on a retrieved record.
Normally, I'd just cache with eager-loading Rails.cache.write('post', Post.includes(:comments).find(99))
. This seems to work, but the problem is I only want to cache a limited subset of the association, and limits are ignored when eager-loading (mentioned here for example). So Post.includes(:popular_comments).find(99)
would return all comments, not just the popular ones.
So I've tried caching the object after lazy-loading the association, but a query unfortunately occurs when pulling the objects out:
class Post < ActiveRecord::Base
has_many :comments
has_many :popular_comments, :class_name > 'Comment', :limit => 20, :order => :votes
post = Post.find(99)
post.popular_comments # lazy-load limited associations
Rails.cache.write('post', post)
...
Rails.cache.read('post').popular_comments # Unwanted SQL query :(
I've tried caching a clone instead, same unwanted SQL query. And I've tried this with both redis and memcached implementations, same result. Oddly, this sequence does work on console afaict, but a simple usage in a controller or view like above fails (ie SQL occurs).
Update (April 2017): I would now say this is a silly premise. Caching whole objects is generally wasteful as it uses a lot of cache storage and it's slow to serialise/deserialise them. Caching associations as well (as asked in this question) is multiplying that wastage by N. Usually it's more efficient to just cache raw IDs and HTML fragments.
Try post.popular_comments.reload
First, limits are in fact ignored when eager loading. From the docs:
If you eager load an association with a specified :limit option, it will be ignored, returning all the associated objects
This means, like you discovered, you have to force the association into the parent object yourself. In my experiments, post.popular_comments
didn't work (this makes sense, since it returns a proxy object), and interestingly neither did post.popular_comments.all
. post.popular_comments(true)
does the trick, however. Underneath that code calls reload, and simply doing post.popular_comments.reload
also gets the association loaded into the parent class.
I'm not sure which of these two is more correct, post.popular_comments(true)
or post.popular_comments.reload
. Both seem a bit brittle, but the second one reads nicer and expresses your intent more clearly.
I validated that both these methods:
My script to store the post:
require 'pp'
Rails.cache.clear
post = Post.first
#post.popular_comments(true)
post.popular_comments.reload
Rails.logger.info "writing to cache"
s = Rails.cache.write "post", post
Rails.logger.info "writing to cache done"
And to retrieve:
require 'pp'
Rails.logger.info "reading from cache"
post = Rails.cache.read "post"
Rails.logger.info "reading from cache done"
Rails.logger.info post.popular_comments.inspect
If I run one after the other my log shows:
Post Load (0.5ms) SELECT `posts`.* FROM `posts` LIMIT 1
Comment Load (0.5ms) SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` = 1 ORDER BY votes LIMIT 20
writing to cache
writing to cache done
reading from cache
reading from cache done
[#<Comment id: 1, ...
My mySQL log also confirms that the second script does not trigger a query for the association.
This was done with Rails 3.1.1
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With