We learn as we go, we write as we learn.

About michelada.io


By joining forces with your team, we can help with your Ruby on Rails and Javascript work. Or, if it works better for you, we can even become your team! From e-Commerce to Fintech, for years we have helped turning our clients’ great ideas into successful business.

Go to our website for more info.

Tags


To Counter Cache or Counter Culture?

14th July 2022

Have you ever worked on a feature that involves lots of record counting? It happened to me.

Think of this scenario: you have multiple Authors with multiple Books, each book has a category and you want to constantly know the number of books of each category of an author.

The first thing I tried was something like Author.books.scifi.count,Author.books.novel.count and Author.books.coaching.count.

Each one of those triggered a query that counts the number of records, in this case, we’re using 3 different scopes but sometimes you can have more than 10 and that can cause a lot of loads.

We can solve that problem in two different ways: using the native Counter Cache function that Ruby on Rails already has OR using a gem named Counter Culture.

Counter Cache

Counter Cache is an ActiveRecord module that helps you cache the count of records associated with another record. So in this case, if we wanted to know just how many books an author has we could use the counter cache function.

For this example, what we have to do is:

  1. Create a column books_count in the authors table.
  2. Go to the Book model and add the option belongs to :author, counter_cache: true.

And that’s it! Now Counter Cache will count automatically the number of books associated to Author.

These are the queries without counter cache:

These are the queries with counter cache

Counter Culture

But in this case, we want to count by book category, so the tool that we’re gonna use for this is the gem Counter Culture.

This gem is going to help you to cache the count of records associated with another record BUT it’s also going to allow you to conditionally do the counting.

First, we have to add the gem to the Gemfile:

gem 'counter_culture', '~> 2.0'

Then we have to create the columns of the resource we’re gonna be counting by.
In this case, we’re gonna create a novels_count, coaching_count, and a scifi_count in the authors table.

Now we’re gonna add the Counter Culture setup line to the Book model.

class Book < ApplicationRecord
  enum category: %i[coaching novel scifi]

  belongs_to :author
  counter_culture :author, column_name: proc {|record| "#{record.category.pluralize}_count" },
    column_names: { novel => 'novels_count', coaching => 'coachings_count', scifi => 'scifis_count' }
end

	# You can also use scopes instead of the array key in the column names, like:
  counter_culture :author, column_name: proc {|record| "#{record.category.pluralize}_count" },
    column_names: { Book.novel => 'novels_count', Book.coaching => 'coachings_count',
                    Book.scifi => 'scifis_count' }

# NOTE: The scopes we used are created by ActiveRecord::Enum

And that’s it, that’s the main configuration.

If you are implementing this with books and authors already created on your database, you have to run this command on your console:

Book.counter_culture_fix_counts

Or if you have multiple counters across multiple models, you might want to create a migration on the Rake task to run the necessary fix counts.

And now you can access this data with 1 query:

Also all the logic can be encapsulated into a service class for your counters.

# app/models/book.rb
class Book < ApplicationRecord
  enum category: %i[coaching novel scifi]

  belongs_to :author
  counter_culture :author, column_name: proc {|record| Counter::Books.dynamic_column_name(record) },
    column_names: Counter::Books::COLUMN_NAMES
end

# app/services/counter/books.rb
class Counter::Books
  COLUMN_NAMES = { Book.novel => 'novels_count', Book.coaching => 'coachings_count', Book.scifi => 'scifis_count' }

  def self.dynamic_column_name(record)
    "#{record.category.pluralize}_count"
  end
end

Conclusion

Counter Cache and Counter Culture are both good for caching the count of records, but it depends on how you want to cache your count. Here's the main difference I see between these two:

  • Counter Cache: Let you cache the total count of records.
  • Counter Culture: This gem lets you cache the total count of records but also allows you to condition that counting.
AUTHOR

Manuel Topete

View Comments