Implementing a Simple Counter Cache in Rails (Ruby on Rails)
Active Record has a functionality where you can cache the count of particular items. But it doesn’t do exactly what I want.
Why would anyone want to cache a count?
Let’s take twitter for example. A user might have 1000s of tweets. Twitter wants to show the number of tweets they have made. If a database query were made, every time the total tweet count was displayed, that would be a very expensive operation. Consequently, it pays to cache that value and to update it as and when required.
Active record increments and decrements everything straight out of the box. Except in my particular case, I didn’t want it decremented. A google search didn’t yield any obvious answers as to how I could handle that situation. A quote
may have many line_items
. We want to keep track of this number in an attribute held on the quote
. Let’s call it: line_items_count
. I could have hacked active record’s implementation, but I thought it far easier to implement my own. I did it using is a simple before_create
call back.
How it works?
When a user creates a line item, we want to store it’s “index” - that is, if it’s the 5th line item created, on a particular quote, then we want its index to read: 5. And then we want to store the total count of all the line items in an attribute on the quote model, and in the quotes table.
Here are the relevant details:
The Schema:
create_table "line_items", force: :cascade do |t|
t.integer "quote_id"
t.integer "item_index"
## Irrelevant details are ommitted
end
create_table "quotes", force: :cascade do |t|
t.integer "line_items_count", default: 0
end
The Migrations:
class AddItemIndexToLineItems < ActiveRecord::Migration[5.2]
def change
add_column :line_items, :item_index, :integer
end
end
class AddLineItemsCountToQuotes < ActiveRecord::Migration[5.2]
def change
add_column :quotes, :line_items_count, :integer, :default => 0
end
end
The models:
class LineItem < ApplicationRecord
belongs_to :quote, inverse_of: :line_items
before_create :update_counts
def update_counts
quote = Quote.find(self.quote_id)
quote.line_items_count = quote.line_items_count + 1
quote.save
self.item_index = quote.line_items_count
end
end
class Quote < ApplicationRecord
include AASM
has_many :line_items, inverse_of: :quote, dependent: :destroy
end
That’s all that you need.
You need to run a rake task to ensure that after you’ve run your migrations, that the database is up-to-date.
namespace :line_items do
desc "update line items with an item index number, update their quotes with a count"
task update_item_index: :environment do
Quote.find_each do |quote|
# WARNING: In my database, I know that each quote has less than 20 line items,
# so doing so is perfectly save. I also know that I have less than 1000 quotes
# in the database, so am perfectly comfortable. If you have more records
# then you will need to run something which will be a little safer.
quote.line_items.order(:created_at).each_with_index do |line_item, index|
line_item.update_columns(item_index: index + 1)
end
quote.update_columns(line_items_count: quote.line_items.count)
end
end
end