Rails Counter Cache Gotcha
Introduction
Today I was attempting to use Rails’ counter cache feature to save on expensive queries. My model’s association is a has_many :through
. I ran into issues on my first attempt but eventually came across the answer after beating my head against a wall for quite some time.
TL;DR: If you use a custom counter_cache
column on your belongs_to
, duplicate that on the has_many
associations as well.
Models
Let’s work with a standard, easy to understand, has_many :through
models:
Counter Cache, Attempt #1
Now let’s implement counter cache on these models with custom column names. On the belongs_to
side of the association, we’ll set up our counter caches for physicians and patients.
Migration
Now, we need to add these counter cache columns with a migration:
This migration will set up our database with these new columns and set values for existing Physicians and Patients via reset_counters
.
Be sure to run rails db:migrate
Test It
After that, we can now test it in the Rails console.
Hmm, from my understanding, the size
method should be using our counter cache, not issuing a SQL COUNT
query in the database. What’s going on here?
Digging into the Documentation
So, it’s not working. Querying the web, it’s difficult to find help with this issue. That is until you dig into the Rails docs, specifically the has_many
method
Scrolling through the options for has_many
, you’ll come across :counter_cache
and it says: “This option can be used to configure a custom named :counter_cache
. You only need this option, when you customized the name of your :counter_cache
on the #belongs_to
association.”
Eureka! We customized the name of our :counter_cache
, so let’s give this a try!
Attempt #2
Let’s revisit the models and make this modification.
Now, we’ll hop back into the Rails console and test it out:
Yay! Now it works!
Note that I added the
:counter_cache
option on bothhas_may
associations. If not, and you tried to dop.appointments.size
, it would query the DB again.
Conclusion
This was a tough nut to crack. Again, most blog posts on the web don’t outline this gotcha. The answer is in the Rails docs but somewhat hidden. I hope this post helps another dev save some time and sanity.