Skipping ActiveRecord cache

Here’s some fun with ActiveRecord cache and how to skip it for an edge case.

A few days ago, I was working on a Sidekiq worker designed to extract several times, in random order – different for each iteration, a few ActiveRecord objects from my app.

At first, it seems easy. You can use the shuffle method or order('random()') on the ActiveRecord collection. In my case, I preferred to use the order method to return an AR collection instead of an array.

But each time, I received my items in the same order. ActiveRecord was caching the request. So, the next step was to skip the ActiveRecord cache for this request:

### You can skip ActiveRecord cache this way
ActiveRecord::Base.connection.uncached do
  User.limit(3).order('random()')
end

But… ActiveRecord was still caching the request! 🤔

Experimentation time!

I wrote this script to test different scenarios:

def users
  ActiveRecord::Base.connection.uncached do
    User.limit(3).order('random()')
  end
end

def uncached_not_working
  puts "------------------ uncached_not_working"
  puts users.ids
  puts "------------------"
  puts users.ids
end

def uncached_working
  puts "------------------ uncached_working"
  puts ActiveRecord::Base.connection.uncached{ users.ids }
  puts "------------------"
  puts ActiveRecord::Base.connection.uncached{ users.ids }
end

Here are the results:

pry(main)> ActiveRecord::Base.connection.cache{ uncached_not_working  }
------------------ uncached_not_working
  User Load (118.1ms)  SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL ORDER BY random() LIMIT $1
117510
160551
103998
79202
4278
------------------
  CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL ORDER BY random() LIMIT $1  [["LIMIT", 5]]
# ⬆️ 😱 
117510
160551
103998
79202
4278
=> nil

In the first case, the request is cached.

pry(main)> ActiveRecord::Base.connection.cache{ uncached_working  }
------------------ uncached_working
  User Load (121.4ms)  SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL ORDER BY random() LIMIT $1 \/*application:****/  [["LIMIT", 5]]
69044
42557
117686
106374
83673
------------------
  User Load (126.4ms)  SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL ORDER BY random() LIMIT $1 \/*application:****/  [["LIMIT", 5]]
149746
104451
68552
145586
91869
=> nil

But in the second case, I have two requests sent to the database. So why my request isn’t cached?

If we look carefully, when you call the users method, the request is not executed. If you consider the two following methods, is the query actually executed in both?

def foo
  User.limit(3)
  "foo"
end

def bar
  User.limit(3)
end

Nope!

pry(main)> foo
=> "foo"
pry(main)> bar
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL LIMIT $1 /*application:****/  [["LIMIT", 3]]
=> [#<User id: ...>]

But why? Because User.limit(3).class = User::ActiveRecord_Relation and you simply never asked to execute the query!

Conclusion

If you want to skip ActiveRecord cache, check if the query is really executed in the block passed to ActiveRecord::Base.connection.uncached.

Good - Cache is skipped

def users
  ActiveRecord::Base.connection.uncached do
    User.limit(3).order('random()').to_a # <- query executed here
  end
end

users

Bad - Cache is not skipped

def users
  ActiveRecord::Base.connection.uncached do
    User.limit(3).order('random()')
  end
end

users.to_a # <- query executed here