1

The special_item_id_list method is responsible for returning an array of ids. The query and logic is complicated enough that I only want to have to run it once per any page request, but I'll be utilizing that resulting array of ids in many different places. The idea is to be able to use the is_special? method or the special_items scope freely without worrying about incurring overhead each time they are used, so they rely on the special_item_id_list method to do the heavy lifting and caching.

I don't want the results of this query to persist between page loads, but I'd like the query ran only once per page load. I don't want to use a global variable and thought a class variable on the model might work, however it appears that the class variable does persist between page loads. I'm guessing the Item class is part of the Rails stack and stays in memory.

So where would be the preferred place for storing my id list so that it's rebuilt on each page load?

class Item < ActiveRecord::Base

  scope :special_items, lambda { where(:id => special_item_id_list) }

  def self.special_item_id_list
    @special_item_id_list ||= ... # some complicated queries
  end

  def is_special?
    self.class.special_item_id_list.include?(id)
  end

end

UPDATE: What about using Thread? I've done this before for tracking the current user and I think it could be applied here, but I wonder if there's another way? Here's a StackOverflow conversation discussing threads! and also mentions the request_store! gem as possibly a cleaner way of doing so.

Community
  • 1
  • 1
Dan Dailey
  • 105
  • 8
  • Rails caches SQL queries by default ...see [here](http://guides.rubyonrails.org/caching_with_rails.html#sql-caching)...is that not the case for your app? – tihom Oct 18 '13 at 00:12
  • 1
    The method involves multiple queries and also some logic to massage the results. Simple SQL caching won't quite cut it here. – Dan Dailey Oct 18 '13 at 00:17

3 Answers3

1

This railscast covers what you're looking for. In short, you're going to want to do something like this:

after_commit :flush_cache

def self.cached_special_item_list
  Rails.cache.fetch("special_items") do
    special_item_id_list
  end
end

private

def flush_cache
  Rails.cache.delete("special_items")
end
Jonathan Bender
  • 1,911
  • 4
  • 22
  • 39
  • I'm not familiar with after_commit, but it seems to run only when a record is changed? I won't be changing the model at all here, just loading data (and actually from several models). Perhaps the Railscast covers it, but I can't watch without a pro subscription. Still, using the built in Rails cache might present an answer. I'm wondering if I might need something at the ApplicationController level to clear out whatever cache method I end up using? – Dan Dailey Oct 18 '13 at 00:43
  • You're only using after_commit to clear your cache (basically if there's ever a change in one of your records, the cache becomes out of date). After the first time you run look it up you'll be hitting the cache rather than making the query (until your next update). – Jonathan Bender Oct 18 '13 at 14:32
  • I'm playing around with your idea this morning to see if it might work. The reason I don't think the after_commit will do the trick, though, is that the results don't change based only on updates to the Item class, but also to it's relations. If one of it's relations change then the cache would not be flushed and the results would no longer be current. I think you may have me on the right track, though. I'll keep fiddling with it. – Dan Dailey Oct 18 '13 at 15:51
  • In that case, you just need to add `touch: true` to your association. For example: `has_many :foos, touch: true` or `belongs_to :bar, touch: true`, then when the associated model is updated the parent will be as well and the cache updated. – Jonathan Bender Oct 18 '13 at 16:31
  • I ended up using Rails cache and just setting a 10 second expiration. Using `touch` is interesting, but ultimately I don't think it would do what I want here. Thanks for your help! – Dan Dailey Oct 18 '13 at 17:48
1

At first I went with a form of Jonathan Bender's suggestion of utilizing Rails.cache (thanks John), but wasn't quite happy with how I was having to expire it. For lack of a better idea I thought it might be better to use Thread after all. I ultimately installed the request_store gem to store the query results. This keeps the data around for the duration I wanted (the lifetime of the request/response) and no longer, without any need for expiration.

Dan Dailey
  • 105
  • 8
0

Are you really sure this optimisation is necessary? Are you having performance issues because of it? Unless it's actually a problem I would not worry about it.

That said; you could create a new class, make special_item_id_list an instance method on that class and then pass the class around to anything needs to use that expensive-to-calculate data.

Or it might suffice to cache the data on instances of Item (possibly by making special_item_id_list an instance method), and not worry about different instances not being able to share the cache.

Robert Kajic
  • 8,689
  • 4
  • 44
  • 43
  • It will tend to be a problem when I'm rendering a potentially long list of items and want to optionally display some indicator based on `is_special` as I loop through. I could do some manual optimization on each such list, but was hoping for a method that would hide away the complexity and allow for laziness. Your suggestion of building my own separate class for cache is something I'd considered, but thought there might be something simpler I didn't know about. – Dan Dailey Oct 18 '13 at 00:39