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?
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.