13

My version is:

  • Rails: 3.2.6
  • dalli: 2.1.0

My env is:

  • config.action_controller.perform_caching = true
  • config.cache_store = :dalli_store, 'localhost:11211', {:namespace => 'MyNameSpace'}

When I write:

 Rails.cache.fetch(key) do
     User.where('status = 1').limit(1000)
 end

The user model can't be cached. If I use

 Rails.cache.fetch(key) do
     User.all
 end

it can be cached. How to cache query result?

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
tinylian
  • 305
  • 1
  • 2
  • 9

4 Answers4

40

The reason is because

User.where('status = 1').limit(1000)

returns an ActiveRecord::Relation which is actually a scope, not a query. Rails caches the scope.

If you want to cache the query, you need to use a query method at the end, such as #all.

Rails.cache.fetch(key) do
  User.where('status = 1').limit(1000).all
end

Please note that it's never a good idea to cache ActiveRecord objects. Caching an object may result in inconsistent states and values. You should always cache primitive objects, when applicable. In this case, consider to cache the ids.

ids = Rails.cache.fetch(key) do
  User.where('status = 1').limit(1000).pluck(:id)
end
User.find(ids)

You may argue that in this case a call to User.find it's always executed. It's true, but the query using primary key is fast and you get around the problem I described before. Moreover, caching active record objects can be expensive and you might quickly end up filling all Memcached memory with just one single cache entry. Caching ids will prevent this problem as well.

Simone Carletti
  • 173,507
  • 49
  • 363
  • 364
  • thx. If I have a category model, it may have less then 100 records, I cache Category.all is a good idea? not cache the cateogry id – tinylian Jun 28 '12 at 06:49
  • Or I first change Category.all to json, and cache them in memcached? – tinylian Jun 28 '12 at 07:06
  • 1
    This is a very important and subtle issue; we just realized this issue had been creeping in to our app -- the difference between `Foo.where(bar: 'fubar')` and `Foo.where(bar: 'fubar').all` is completely different w/r/t caching, and something that can easily and subtly slip into your codebase. Check and check regularly if you rely on caching. – Tom Harrison Oct 17 '13 at 02:18
  • Good explanation of why caching may not always be a good idea. One issue I ran into is that cached results aren't upgraded during a migration unless you explicitly expire the cached objects as part of the migration. For instance, if you add a migration that adds a new field with a default value and pass in `:null => false` (effectively upgrading all the rows in the table with the default value), records from the cache will return a nil value, not the default, until you expire the cached records and force them to be fetched again. – Chris Bloom Oct 27 '13 at 15:47
  • As always with caching you should also be very aware of your invalidation/sweeping plan. – Sam Figueroa Dec 18 '13 at 09:10
  • 3
    I'd like more evidence that it's *never* a good idea to cache ActiveRecords (even though the alternative of `find(ids)` is quite quick anyway, as you point out). Especially if they're being used read-only for display purposes - perhaps the most common usage - I'm not sure it can do much harm in most cases. Perhaps there's some risk of broken links if an association changes, requiring some case-by-case analysis, but I think that's rare in reality. – mahemoff May 17 '14 at 20:27
  • me too @mahemoff i read [here](http://guides.rubyonrails.org/caching_with_rails.html#low-level-caching) `Sometimes you need to cache a particular value or query result instead of caching view fragments. Rails' caching mechanism works great for storing *any* kind of information.` I assume query result would include ActiveRecord::Relations and 'any' is in bold – ryan2johnson9 Jul 29 '16 at 01:31
  • So if we want to cache an ActiveRecord::Relation on `find()`, what can we use? My find query is expensive, I want to cache it -- nobody has answered how I can do it. – Kelsey Hannan Mar 15 '17 at 01:21
3

In addition to selected answer: for Rails 4+ you should use load instead of all for getting the result of scope.

2

Rails.cache.fetch caches exactly what the block evaluates to.

 User.where('status = 1').limit(1000)

Is just a scope, so what gets cached is just the ActiveRecord::Relation object, ie the query, but not its results (because the query has not been executed yet).

If you want something useful to be cached, you need to force execution of the query inside the block, for example by doing

User.where('status = 1').limit(1000).all

Note that on rails 4 , all doesn't force loading of the relation - use to_a instead

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
0

Using

User.where("status = 1").limit(1000).all

should work.

moritz
  • 25,477
  • 3
  • 41
  • 36