Stay Ahead in Ruby!
From development processes to the most useful gems, get it all straight in your inbox. Join our Ruby blog today!

Skip to main content

Boost Your Rails App's Performance with Counters: counter_cache vs counter_culture

Boost Your Rails App's Performance with Counters: counter_cache vs counter_culture - cover image

Counters are a common feature in Rails applications that help you keep track of the number of associated records for a given model. In Rails, there are two popular ways to implement counters: counter_cache and counter_culture. Both approaches can significantly improve the performance of your application, but they have some important differences that developers need to be aware of.

In this article, we will explore the key features of counter_cache and counter_culture, their advantages and disadvantages, and when to use one over the other. We will also discuss some common issues with counters in Rails and how to mitigate them.

By the end of this article, you should have a better understanding of counters in Rails and be able to choose the best approach for your specific use case.

Counter_cache overview #

In Rails, counter_cache is a feature that allows you to cache the count of associated records automatically. It stores this count in a separate column within the parent model’s table. This can be a powerful tool for improving the performance of Rails applications that involve associations with large numbers of associated records.

When you add counter_cache: true to a has_many or has_and_belongs_to_many association, you need to create a separate column in the parent model’s table to store the count of associated records.

For example, if you add counter_cache: true to a has_many :comments association in a Post model, so you should create a comments_count column in the posts table.

Tip: The field name can be overridden by using a symbol instead of true as the value for the counter_cache option.

Whenever a new associated record is created or destroyed, Rails will update the cached count in the parent model’s table. This happens automatically behind the scenes, without you having to write any additional code.

Has_many example #

Suppose you have a Rails application with a Post model and a Comment model, where each post can have many comments. You can use counter_cache to cache the number of comments for each post, like this:

class Post < ApplicationRecord
has_many :comments, dependent: :destroy, counter_cache: true
end

class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
end

In this example, we have added counter_cache: true to both the has_many and belongs_to associations. This tells Rails to automatically cache the count of comments for each post in a comments_count column in the posts table.

For example, if you wanted to display a list of posts with the number of comments on each post, you could use the comments_count attribute like this:

@posts = Post.all.order(created_at: :desc)
<% @posts.each do |post| %>
  <div class="post">
    <h2><%= post.title %></h2>
    <p><%= post.body %></p>
    <p><%= post.comments_count %> comments</p>
  </div>
<% end %>

In this example, the post.comments_count attribute will retrieve the cached count of comments for each post, rather than executing a separate SQL query to count the associated comments for each post.

Has_and_belongs_to_many example #

Suppose you have a Rails application with a User model and a Group model, where each user can belong to many groups and each group can have many users. You can use counter_cache to cache the number of users in each group and the number of groups that each user belongs to, like this:

class User < ApplicationRecord
has_and_belongs_to_many :groups, counter_cache: true
end

class Group < ApplicationRecord
has_and_belongs_to_many :users, counter_cache: true
end

In this example, we have added counter_cache: true to both the has_and_belongs_to_many associations. This tells Rails to automatically cache the count of users for each group in a users_count column in the groups table, and to cache the count of groups for each user in a groups_count column in the users table. Now, whenever you access the users_count or groups_count attribute on a Group or User object, Rails will retrieve the cached count instead of executing a separate SQL query to count the associated records.

For example, if you wanted to display a list of groups with the number of members in each group, you could use the users_count attribute like this:

@groups = Group.all.order(:name)
<% @groups.each do |group| %>
  <div class="group">
    <h2><%= group.name %></h2>
    <p><%= group.users_count %> members</p>
  </div>
<% end %>

Overall, the counter_cache feature can be especially useful when dealing with large numbers of associated records, as it allows Rails to avoid executing a separate SQL query to count the associated records for each parent record. By caching the count of associated records in a separate column in the parent model’s table, Rails can significantly reduce the overall number of database queries and improve the performance of your application.

counter_cache: pros/cons #

Here are some pros and cons of using counter_cache in your Rails application:

Pros:

  • counter_cache is a built-in Rails feature, so it’s easy to set up and use.
  • It’s a simple and efficient way to implement basic counters, such as the number of comments on a blog post.
  • It can improve performance by reducing the number of database queries needed to retrieve counter information.
  • It’s useful for cases where you only need to know the total count, rather than detailed information about each associated record.

Cons:

  • It’s limited to simple counter functionality and can’t be customized for more complex use cases.
  • It can become unwieldy if you have complex associations or need more granular control over your counters.
  • It requires additional configuration in your models and database schema.
  • It can be prone to caching issues, especially if you’re using a lot of caching in your application.

Counter_culture overview #

counter_culture is a gem that provides an alternative way to cache the count of associated records. It allow to create more advanced things like conditional counter cache, customized column name, dynamic column name, etc. This can be useful in certain situations where you need more flexibility or control over how the count is cached.

To use counter_culture, you first need to add it to your Rails project by adding it to your Gemfile and running bundle install. Once you’ve done that, you can add counter_culture to your models like this:

class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end

class Comment < ApplicationRecord
belongs_to :post
counter_culture :post
end

Also, you need to create a migration to add comments_count column to Post model.

Now, the Post model will keep an up-to-date counter-cache in the comments_count column of the post table.

For example, you create a new comment for a post like this:

post = Post.find(1)
post.comments.create(body: "Great post!")

When the new comment is created, counter_culture will automatically update the comments_count column to reflect the new count of comments for the post.

Similarly, when a comment is destroyed, counter_culture will automatically update the comments_count column to reflect the new count of comments for the post.

More complex example #

Let’s say you have a social media platform that allows users to post content, and each post can have many comments and reactions (likes, dislikes, etc.). You want to display the number of comments and reactions for each post, but you also want to allow users to filter posts by the total number of reactions across all posts. To accomplish this, you can use counter_culture to update counters for each post and for the user’s total reactions.

First, you’ll need to add the counters to your Post and User models:

class Post < ApplicationRecord
belongs_to :user
has_many :comments
has_many :reactions

counter_culture :user, column_name: 'total_reactions', delta_column: 'reaction_count'
counter_culture :comments, column_name: 'comment_count'
counter_culture :reactions, column_name: 'reaction_count'
end

class User < ApplicationRecord
has_many :posts
end

In this example, we’ve added three counter_culture statements to the Post model:

  • counter_culture :user: This updates a counter on the associated User model called total_reactions, which keeps track of the total number of reactions across all of the user’s posts. We’re specifying column_name: 'total_reactions' to set the name of the counter column in the User table, and delta_column: 'reaction_count' to specify that we want to use the reaction_count column on the Post model to calculate the change in the counter value.
  • counter_culture :comments: This updates a counter on the Post model called comment_count, which keeps track of the number of comments on each post.
  • counter_culture :reactions: This updates a counter on the Post model called reaction_count, which keeps track of the number of reactions (likes, dislikes, etc.) on each post.

With these counters in place, you can now easily display the number of comments and reactions for each post, as well as the total number of reactions for each user. For example:

<% @posts.each do |post| %>
  <div class="post">
    <h2><%= post.title %></h2>
    <p><%= post.content %></p>
    <p>Comments: <%= post.comment_count %></p>
    <p>Reactions: <%= post.reaction_count %></p>
  </div>
<% end %>

<% @users.each do |user| %>
  <div class="user">
    <h2><%= user.name %></h2>
    <p>Total reactions: <%= user.total_reactions %></p>
  </div>
<% end %>

Finally, to allow users to filter posts by the total number of reactions across all posts, you can add a scope to your Post model:

class Post < ApplicationRecord
# ...
scope :by_total_reactions, -> { joins(:user).order('users.total_reactions DESC') }
end

Now you can use this scope to filter posts:

@popular_posts = Post.by_total_reactions

This example demonstrates how you can use counter_culture to update counters across multiple models and display aggregated data.

Of course, there are more complex things that can be done using counter_culture gem. It has good documentation and there are a lot of examples.

The difference between counter_culture and counter_cache #

Here are some key differences between the two approaches:

  • Dynamic columns: counter_culture supports dynamic column names, making it possible to split up the counter cache for different types of objects.
  • Control: With counter_culture, you have more control over how the count is cached and can customize the caching behavior in various ways, such as excluding certain records from being counted or counting records based on custom conditions. counter_cache provides less control over the caching behavior.

Overall, both counter_cache and counter_culture are useful features in Rails for caching the count of associated records and improving the performance of your application. Which one you choose to use depends on your specific requirements and preferences.

Potential issues #

When working with counters in Rails, there are several potential issues you should be aware of and ways to deal with them:

  • Race conditions: If multiple requests try to update a counter at the same time, it’s possible for the counter to be incremented or decremented incorrectly. One way to deal with this is to use database-level locks or optimistic locking to ensure that only one request can update the counter at a time.
  • Caching: If you’re using caching in your application, you’ll need to make sure that the cached value of the counter is updated whenever the counter changes. This can be done using cache sweepers or other cache invalidation techniques.
  • Inconsistent data: If you have a lot of associations between models and you’re using counters to keep track of them, it’s possible for the counter values to become inconsistent due to errors or database issues. One way to deal with this is to periodically recalculate the counter values to ensure that they are accurate.
  • Performance: If you have a lot of counters in your application, updating them can be a performance bottleneck. One way to deal with this is to use background jobs or other asynchronous processing techniques to update the counters outside of the request-response cycle.
  • Unnecessary complexity: Sometimes, using counters can add unnecessary complexity to your code. If you find that you’re spending a lot of time dealing with counter-related issues, it may be worth reconsidering whether counters are the best way to solve your problem.

By being aware of these potential issues and using best practices to deal with them, you can ensure that counters in your Rails application are accurate and reliable.

Final thoughts #

When deciding which approach to use, it’s important to consider your application’s specific requirements and the complexity of the associations between your models. counter_cache is a simple and efficient way to implement basic counters, but it can become unwieldy if you have complex associations or need more granular control over your counters. counter_culture provides more flexibility and functionality, but it comes with a higher learning curve and requires more configuration.

Overall, counters are a powerful tool for keeping track of data in Rails applications, and by choosing the right approach and being aware of potential issues, you can use them effectively to build robust and scalable applications.

We are ready to provide expert's help with your product
or build a new one from scratch for you!

Contact MobiDev’s tech experts!