Adding counter caches to existing models in Rails
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.