How to Start Using Counter Caches in Rails
It is widespread to have parent-child associations in Rails applications. On the parent side is a :has_many
association, and on the child side is a :belongs_to
association. Examples include an article with comments, or an author with books--the former is the parent, and the latter is the child.
It is often useful to display a count of the children alongside information about the parent, without necessarily loading all the child records. For example, if an application has articles with comments, users looking at an article list will want to know which articles have the most comments.
Using the counter_cache
in this situation will result in fewer requests to the database and, therefore, generally better performance.
Use these links to skip ahead in the guide:
Rails Counter Cache Explained
The counter cache may seem mysterious, but it’s not that complicated. It’s just a database column storing the number of children, with the value automatically updated.
Counter Cache Definition
For any parent model with an associated child model, the counter_cache
is an integer database column with the value maintained by the Rails framework. An application should treat it as read-only. The column contains a pre-calculated number of associated children.
Note that the counter_cache
is different from many of the other Rails caches, which have specialized stores (such as the filesystem, memory, an external key-value store). The value of the counter_cache, on the other hand, is stored in the database. The benefit is that the application almost never needs to query the child database table for the count of children.
How Do Counter Caches Work?
Whenever any code in a Rails application changes the number of children of a parent model, the value of the counter_cache
column is automatically updated. So when the number of associated child models increases (for example, a comment is added to an article), the counter_cache
automatically increments. When the number of associated child models decreases (a comment is removed from an article, perhaps through admin moderation), the counter_cache
automatically decrements.
When displaying the parent, no additional database queries are required to get the number of children. The count of children is already present in the parent object.
Getting Started
Let’s use a simple system as an example. Consider an Article
model, which has a child Comment model (the Comment:belongs_to
the Article
and the Article:has_many
Comments). When the list of Articles
is displayed, the system should also show the number of Comments
. We expect a lot of user-submitted comments for some articles and a few for others. We also know that this is a read-heavy site, so we want to reduce the number of database queries.
This is a perfect place to use the counter_cache
.
Note that this is a classical trade-off between storage space and time. The Article
model will take slightly more space (because it has a new column with the number of children), but by pre-calculating the number of children at Comment
creation time, we decrease the time needed to display that value.
Follow along step-by-step as we learn how to use counter caches:
Set up the child model
First, add the option to the child model (in this case, the Comment). Edit the child model file and modify the :belongs_to
section.
Set up the parent table
Add a column to the parent database table (articles
) by creating and running a migration.
Reset the counter cache (optional)
Sometimes, the counter_cache
value for existing records will need to be updated. When we add the column to the articles table, it will have the default value (typically 0). This is fine if the system has no articles, but the counter_cache
is being added to help improve performance, make sure that all existing articles are updated with the number of their comments.
Display the number of children
Finally, access the counter_cache
value in the application’s views (or any other display component) by using article.comments.size
.
When we use this, Rails won’t query the comments table, unlike other methods of finding the count of the children. Some other methods to find the number of comments include:
- The
article.comments.count
method, which always performs acount(*)
on thecomments
table. - The
article.comments.length
method, which loads all the child comments into memory (unless they’ve been previously loaded) and then gets the length of the array. - The
article.comments.size
method without usingcounter_cache
, which queries the database unless the comments array is already in memory, having been previously loaded from the database by another code path.
Counter Caching Example
Let’s examine some code! We’ll use the same models and relationships we walked through in the Getting Started section. As a reminder, we have an Article model (the parent) that has many Comments (the children), and each Comment belongs to one and only one Article.
First, we need to make sure we have our relationship set up correctly (without using the counter_cache
):
$ cat article.rb
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
end
$ cat comment.rb
class Comment < ApplicationRecord
belongs_to :article
end
Next, add the counter_cache option to the Comment model (add the text in bold):
$ cat comment.rb
class Comment < ApplicationRecord
belongs_to :article, counter_cache: true
end
Then, we need to create and run a migration that will add the column for the cached count:
$ rails g migration AddCommentsCountToArticles comments_count:integer
This creates this migration:
class AddCommentsCountToArticles < ActiveRecord::Migration[6.0]
def change
add_column :articles, :comments_count, :integer
end
end
Run the migration:
$ rails db:migrate
Note that parent table column name (comments_count
) typically matches the naming convention of <child_table_name>_count
. If, for some reason (legacy compatibility, perhaps), we need a different column name, specify a different column name in the model file.
To use the total_comments
column as the counter cache, this is what the:belongs_to section
of the Comment
model would look like:
belongs_to :article, counter_cache: :total_comments
Next, we want to update the column value for existing records. If there are no existing Articles
, skip this step. We want to do this across all environments, which is why we do this via a migration.
$ rails g migration ResetAllArticleCacheCounters
Next, update the generated migration file to this:
class ResetAllArticleCacheCounters < ActiveRecord::Migration[6.0]
def up
Article.all.each do |article|
Article.reset_counters(article.id, :comments)
end
end
def down
# no rollback needed
end
end
Run our migrations:
$ rails db:migrate
Finally, we can access the child count in a view file, and the value of the counter_cache column will be displayed, and the comments table will not be queried:
<%= article.comments.size %>
Gotchas
If the application adds or removes child records but doesn’t use Rails, the counter_cache
column won’t be updated--this might occur with an external data loading process. If this is the case, the process can run code similar to the ResetAllArticleCacheCounters migration at the end of each data load.
The counter cache by default decrements when a child model object is destroyed. If the application uses a soft delete gem such as paranoia, the counter cache may or may not be updated. Make sure to review the relevant gem’s documentation to verify compatibility.
A given model can have multiple counter caches. For example, an article might have a counter_cache
for comments and a counter_cache
for citations.
Never modify the counter_cache
column from the Rails code. It should only be modified by the reset_counters
method.
As mentioned above, when using the counter cache, you are introducing some complexity, just like any other cache. Adding or removing children is going to take slightly longer, and the parent model will be slightly larger. That’s the trade-off to have the number of children stored on the parent model. As always, examine real-world usage and benchmark it to make sure the correct trade-offs are made.
Conclusion
The counter cache is a useful Rails framework feature. If an application has a parent-child association and renders, the parent’s child count often, a counter cache is an easy and performant way to do so.