cd ..

ActiveRecord Increment


Ari profile picture

Ari Summer / July 24, 2022

Subscribe to receive new post previews, ideas,
and tips & tricks in your inbox.

ORMs like ActiveRecord are great, but sometimes they make things so easy that we're not thinking about what's happening underneath, resulting in hard to track down bugs.

Race conditions are particularly hard to track down and reason about. If you Google for how to increment a column in Rails, you'll see a lot of solutions using #increment and #increment!. If you look at the source for these methods, you'll see:

def increment(attribute, by = 1)
self[attribute] ||= 0
self[attribute] += by
self
end
def increment!(attribute, by = 1)
increment(attribute, by).update_attribute(attribute, self[attribute])
end

What's the problem? Well let's say we are incrementing a click count whenever a user clicks on a page:

class PagesController < ApplicationController
def click
@page = Page.find(params[:id])
@page.increment!(:clicks, 1)
end
end

Now let's imagine two requests come in at the exact same time. Since #increment is incrementing the column in-memory, we have a race condition! Both requests could increment the column at the same time, in-memory, resulting in a count that only increments by one instead of two.

How do we avoid this race condition? We can push the concern of incrementing to the database. Rather than incrementing in memory, let's have the database do it:

Page.where(id: params[:id]).update_all("click = click + 1")

This results in the following SQL:

UPDATE "pages" SET count = count + 1 WHERE "pages"."id" = $1;

Updates like this are atomic and concurrent requests won't be a problem. In our example above, the click count would be incremented by two, just as it should!

Note that this is exactly what the lesser-known #update_counters method does. Next time you need to increment or decrement some database columns, I hope this tip comes in handy!

Subscribe to receive new post previews, ideas,
and tips & tricks in your inbox.