Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Caching limited associations

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.

like image 224
mahemoff Avatar asked Feb 03 '12 08:02

mahemoff


1 Answers

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:

  1. stored the limited association in memcache
  2. did not trigger a SQL query after loading from cache

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

like image 155
avaynshtok Avatar answered Sep 30 '22 12:09

avaynshtok