0

I am struggling to get a counter cache working on a has_one :through and running into some weird behaviour. I want to have a counter_cache on the Company model that caches the number of associated Location (through the BusinessUnit model).

Objects:

class Company < ApplicationRecord has_many :business_units, class_name: :BusinessUnit, dependent: :destroy has_many :locations, through: :business_units, dependent: :destroy, counter_cache: :locations_count end class BusinessUnit < ApplicationRecord belongs_to :company, counter_cache: :locations_count has_many :locations, dependent: :destroy end class Location < ApplicationRecord belongs_to :business_unit has_one :company, through: :business_unit end 

Schema:

ActiveRecord::Schema[7.1].define(version: 2025_04_03_203549) do enable_extension "plpgsql" create_table "business_units", force: :cascade do |t| t.string "name" t.bigint "company_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["company_id"], name: "index_business_units_on_company_id" end create_table "companies", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "locations_count", default: 0, null: false end create_table "locations", force: :cascade do |t| t.string "name" t.bigint "business_unit_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["business_unit_id"], name: "index_locations_on_business_unit_id" end add_foreign_key "business_units", "companies" add_foreign_key "locations", "business_units" end 

My understanding based on these SO answers (1, 2) is that this should work. It's not well documented and a bit counterintuitive how Rails automatically knows to refer to the locations count, but indeed it does seem to have some rails magic and calling Company.reset_counters(1, :locations) correctly sets the counter to match the number of locations.

HOWEVER, the counter doesn't update automatically (up or down) when locations are added or removed, and if a BusinessUnit with N locations is updated and assigned to another company the counter only increments/decrements by 1 (instead of by the expected N locations). This behaviour is present on both Rails 8.0.1 and 7.1.3 (which I accidentally used to build my MRE).

Is there something obvious I'm missing that can make the counter_cache work correctly?

1
  • I wouldn't take those answers as proof that it actually works. One is 13 years old and the other is just a false solution where the asker has a bad method of verifying that it actually worked. SO has tons of those and self answered questions with low upvotes tend to be very low quality. Commented Apr 4 at 20:03

1 Answer 1

2

This won't actually work.

The counter_cache option only really works on belongs_to associations* and will update the column on the model on the other side of it which is derived from the name of the association. On the has_many side it's just used if the name of the counter cache column doesn't match the automatically derived name when using the cache.

What you want to do here is pretty far from the narrow use case the counter_cache option is designed for.

If you want to combine this database design with a cached count of the indirect associations you'll have to look into other options like the counter culture gem or incrementing the column with a association callback or explicitly from the controller.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks. This was the conclusion I was coming to. It's very confusing because reset_counters appears to work (i.e. sets the counter equal to the number of locations, instead of the number of business units). I don't really understand why it behaves that way if the use case is not intended to be supported. I was hoping to avoid adding another gem but counter culture seems to be the way that everyone else is actually solving this. Much appreciated.
@NGobin could it be that the number was 1:1?
Nope. If you set it up with 1 company has 1 business unit which has 3, 5, or N locations, and then call Company.reset_counters(1, :locations) it will set to 3, 5, or N. I tested this several times with my Minimum Reproducible Example and it worked every time (and I went and made the MRE for this post based on observing the behaviour in a development branch of my app).
Its kind of surprising as there is nothing in the API docs that would explain it working that way.
Indeed it is surprising, hence the question. Looking at the source only further mystifies me, as the block to handle an ActiveRecord::Reflection::ThroughReflection implies that the reset_counters method was specifically written to handle _through associations, which implies that it should not be just limited to belongs_to associations. But it's a challenging method for me to reason through and probably there are reasons / edge cases that I'm missing.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.