Michael Anhari

Adding counter caches to existing models in Rails

5 vertical stacks of various coins

Grabbing the count of related records tied to a parent is a common task in web development. For example, displaying the number of comments on a blog post, or the amount of deliveries your expecting this afternoon at your warehouse. For our post today, let's say we have a Topic model that can have many Article records.

class Topic < ApplicationRecord
  has_many :articles
end

class Article < ApplicationRecord
  belongs_to :topic
end

To the database!

Using the .count method from ActiveRecord will always send a request to your underlying database:

Topic.first.articles.count
# => SELECT COUNT(*) FROM "articles" WHERE topic_id = 1;

Caching the value to avoid extraneous trips

Rails ships with caching the values for these count requests onto the parent model as relations_count integer columns. In our case, we could add articles_count to our Topic model and populate the counts:

class AddArticlesCountToTopics < ActiveRecord::Migration[6.0]
  def up
    add_column :topics, :articles_count, :integer, default: 0, null: false

    execute <<-SQL
      UPDATE topics SET articles_count = (
        SELECT COUNT(*) FROM article WHERE topic_id = topics.id
      )
    SQL 
  end

  def down
    remove_column :topics, :articles_count
  end
end

then, all we need to do is let Rails know to that this value needs to be cached on the belongs_to method call in Article:

class Article < ApplicationRecord
  belongs_to :topic, counter_cache: true
end

Done! Rails will now keep our topics.articles_count number up to date, and we can use topic.articles_count instead of topic.articles.count to avoid a trip to the database.

Newsletter

I'm working on sending out a weekly newsletter. I'll make it as easy as possible to unsubscribe at any time.