Usually, discovering n+1 problems in your Rails application that can’t be fixed with an :include statement means lots of changes to your views. Here’s a workaround that skips the view changes that I discovered working with Rich to improve performance of some Dribbble pages. It uses memoize to convince your n model instances that they already have all the information needed to render the page.
While simple belongs_to relationships are easy to fix with :include, lets take a look at a concrete example where that won’t work:
class User < ActiveRecord::Base has_many :likes end class Item < ActiveRecord::Base has_many :likes def liked_by?(user) likes.by_user(user).present? end end class Like < ActiveRecord::Base belongs_to :user belongs_to :item end
A view presenting a set of items that called Item#liked_by? would be an n+1 problem that wouldn’t be well solved by :include. Instead, we’d have to come up with a query to get the Likes for the set of items by this user:
Like.of_item(@items).by_user(user)
Then we’d have to store that in a controller instance variable, and change all the views that called item.liked_by?(user) to access the instance variable instead.
Active Support’s memoize functionality stores the results of function calls so they’re only evaluated once. What if we could trick the method into thinking it’s already been called? We can do just that by writing data into the instance variables that memoize uses to save results on each of the model instances. First, we memoize liked_by:
memoize :liked_by?
Then bulk load the relevant likes and stash them into memoize’s internal state:
def precompute_data(items, user) likes = Like.of_item(items).by_user(user).index_by {|like| like.item_id} items.each do |item| item.write_memo(:liked_by?,likes[item.id].present?,user) end end
The write_memo method is implemented as follows.
def write_memo(method, return_value, args=nil) ivar = ActiveSupport::Memoizable.memoized_ivar_for(method) if args if hash = instance_variable_get(ivar) hash[Array(args)] = return_value else instance_variable_set(ivar, {Array(args) => return_value}) end else instance_variable_set(ivar, [return_value]) end end
This problem described here could be solved with some crafty left joins added to the query that fetched the items in the first place, but when there’s several different hard to prefetch properties, such a query would likely become unmanageable, if not terribly slow.
Nice article, thanks for describing the technique.
I’ve been doing something similar (well, similar problem) using ivar ‘caching’ (not sure what to call this!) in the method calls, i.e.:
def something_heavy(opts)
@something_heavy ||= the_heavy_method(opts)
..
@something_heavy
end
Are there any pitfalls (practical / performance) using either approach you think?