Avoiding Caching with Rails ActiveRecord
Today, I came across a puzzling issue with Rails 4.1, ActiveRecord, and Postgres when trying to select random records from a database table.
To demonstrate, let’s use a simple social network API example. Clients will POST a list of user, and I’ll “match” them to someone in my database.
Easy enough, for testing purposes we can just select a random user from our “users” table in postgres to simulate a match.
1 contacts = [{:id => 1}, {:id => 2}, {:id => 3}, {:id => 4}]
2 contacts.each do |c|
3 user = User.order('random()').first
4 c[:detail] = {:username => user.username}
5 end
Run that in our rails console, and good things happen. Seems like we have some random users from my (small) database.
1 [{:id=>1, :detail=>{:username=>"adam5"}},
2 {:id=>2, :detail=>{:username=>"adam1"}},
3 {:id=>3, :detail=>{:username=>"adam11"}},
4 {:id=>4, :detail=>{:username=>"adam5"}}]
Let’s move that code into a controller action.
1 def create
2 contacts = [{:id => 1}, {:id => 2}, {:id => 3}, {:id => 4}]
3 contacts.each do |c|
4 user = User.order('random()').first
5 c[:detail] = {:username => user.username}
6 end
7 render :json => contacts, :root => false
8 end
Uh-oh. This time, we get a decidedly un-random response:
[
{
"id": 1,
"detail": {
"username": "adam6"
}
},
{
"id": 2,
"detail": {
"username": "adam6"
}
},
{
"id": 3,
"detail": {
"username": "adam6"
}
},
{
"id": 4,
"detail": {
"username": "adam6"
}
}
]
Looking at the logs, we’ll see this:
User Load (0.7ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
User Load (0.7ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY random() LIMIT 1
Rails caching, normally so useful, causes a problem in this situation. No sweat. You just need to use ActiveRecord’s uncached method.
Modifying the action to use uncached looks like this:
1 def create
2 contacts = [{:id => 1}, {:id => 2}, {:id => 3}, {:id => 4}]
3 contacts.each do |c|
4 User.uncached do
5 user = User.order('random()').first
6 c[:detail] = {:username => user.username}
7 end
8 end
9 render :json => contacts, :root => false
10 end
The database query no longer gets cached for the duration of the “User.uncached” block, and a new query is executed for each iteration. The controller action now gives us some nice, randomized output.
[
{
"id": 1,
"detail": {
"username": "adam3"
}
},
{
"id": 2,
"detail": {
"username": "adam3"
}
},
{
"id": 3,
"detail": {
"username": "adam4"
}
},
{
"id": 4,
"detail": {
"username": "adam1"
}
}
]