Rails 5.2.x --> Rails 6 counter_cache Gotcha
Tags: rails, ruby, migration, upgrade
Published: 2019-08-22 16:04:51

---
title: Rails 5.2.x --> Rails 6 counter_cache Gotcha
published: true
description: Migrations are hard
tags: rails, ruby, migration, upgrade
cover_image: https://images.unsplash.com/photo-1512668033346-09cfb0d8597b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2641&q=80
---

I recently upgraded our production app from Rails 5.2.3 to Rails 6.0.0. We wanted access to all the shiny things everyone is talking about. Multiple databases! Better autoloading! 

Everything was going great until I noticed one weird behavior where a count column wasn't getting updated, but all the associations were correctly updating. After talking with a coworker about the problem, we identified what was going on. The culprit? Rails ActiveRecord `counter_cache`. 

This post will give a brief overview of `counter_cache` and how its behavior changed between the two Rails versions, and finally, how to update your code to get around this issue without throwing `counter_cache` out the window. 

### Counter Caching - What is it? Why would I use it?
This concept was new-to-me - which is great! I love learning new things and working towards understanding why this approach is better over others. In this case, it was older code and we couldn't ask the original writer why they made this decision. So off to the internet I went. 

I found quite a few articles about how to implement `counter_cache` but was surprised to see that none of them were from official Rails documentation - which was interesting. 

That said, I think the best resource I found was [this (very old) Railscast episode](http://railscasts.com/episodes/23-counter-cache-column) about converting fetching the count on a record and its relations to counter caching in the database. I highly recommend watching it - but here's the short answer of why you would choose to implement counter caching: 

> A counter cache column stores the count of its associations on itself - which means you don't have to make extra database calls = more performant

### Old Behavior & Implementation
Let's say we have a cookie jar and each cookie jar has many cookies inside it. Somewhere in our UI, we want to show how many cookies are in each cookie jar.

A simple implementation of setting up our models might look like this: 
```ruby
# cookie_jar.rb
class CookieJar
  has_many :cookies
end

# ## Schema Information
# Table name: `cookie_jars`
# ### Columns
#
# Name                 | Type               | Attributes
# -------------------- | ------------------ | ---------------
# **`id`**             | `bigint(8)`        | `not null, primary key`
# **`cookies_count`**  | `integer`          | `default(0), not null`

# cookie.rb
class Cookie
  belongs_to :cookie_jar, counter_cache: true
end

# ## Schema Information
# Table name: `cookies`
# ### Columns
#
# Name                 | Type               | Attributes
# -------------------- | ------------------ | ---------------
# **`id`**             | `bigint(8)`        | `not null, primary key`
# **`cookie_jar_id`**  | `integer`          | `not null`
```
So that means that we now have `CookieJar`, `Cookie` and the `cookie_jars` table has a column called `cookies_count` that uses `counter_cache` to calculate those values.

So now the tricky part - `counter_cache` is doing some work behind the scenes, which makes it easy to use, but more challenging to debug. In this case, our bug source was in some seed code that looked like this: 
```ruby
# seed_cookie_jar.rb
def assign_cookies
  jar = CookieJar.first
  jar.cookies.each do |cookie|
    cookie.update!(cookie_jar_id: jar.id)
  end
end

``` 
_Just a note that in this example, let's not worry about where our cookies were created - just that they previously had a different `cookie_jar_id` that we want to change here in `assign_cookies`._

So, before Rails 6, this behavior worked - I move my first cookie from one jar to another and the old jar now has one less `cookies_count` and the new jar has incremented its `cookies_count`. 

Our Cookie Jar table before we call `assign_cookies`


|id|cookies_count|
|:-----|:-----|
|1|4|
|2|0|

What our Cookie Jar table looks like after we move the cookies around

|id|cookies_count|
|:-----|:-----|
|1|0|
|2|4|

Everything is working as expected! All 4 of our cookies moved from Cookie Jar 1 to Cookie Jar 2. Delightful. Now let's wreck havoc on this and upgrade to Rails 6 :muscle:

### New Behavior & Changes Made
To be fair, there was a one-line reference to the pull request that implemented the new behavior. However, what surprised me was that the way the change was phrased in the changelog did not at all alert me that my code was now essentially broken. 

Here's the changelog note: "Don't update counter cache unless the record is actually saved". Yup, that's it. Now, I'm not a Ruby or Rails pro, so maybe this was obvious behavior to other people...but boy-o it took me for a ride! 

Without doing anything to our code, here was the bug we had:
Our Cookie Jar table before we call `assign_cookies` (same as last time)


|id|cookies_count|
|:-----|:-----|
|1|4|
|2|0|

What our Cookie Jar table looks like after we invoke `assign_cookies` - no errors, but those counts don't look right :thinking:

|id|cookies_count|
|:-----|:-----|
|1|4|
|2|0|

More confusingly, what our Cookies table looks like after we move the cookies: 

|id|cookie_jar_id|
|:-----|:-----|
|1|2|
|2|2|
|3|2|
|4|2|

WHAT?! Our cookies actually moved jars, but the counter didn't update. So in our UI, we're displaying that Cookie Jar 1 has 4 cookies in it, but when you click to see the detail of that Cookie Jar 1 - it's empty! 

To be honest, the way my coworker and I figured this out was going to the pull request listed in the changelog as changing this code and looking at the code diffs, new tests cases, and try some stuff out. All that eventually led to us trying one thing: instead of passing an id value, pass the whole record to the block. (Spoiler alert - this worked!) Here's that code: 
```ruby
# seed_cookie_jar.rb
def assign_cookies
  jar = CookieJar.first
  jar.cookies.each do |cookie|
    cookie.update!(cookie_jar: jar)
  end
end
```

So no more `cookie_jar_id` set directly - let's pass it the whole jar instance and see what happens. Turns out, this does call a save on the `jar` instance and triggers the cache to update. 

---

Thanks for sticking with me this far - hope you learned something new and are more cautious about updating association `_id` fields now! I definitely will think twice before passing in an explicit `_id` value vs. the instance from now on. 

---

Photo by [Brooke Lark on Unsplash](https://unsplash.com/@brookelark)