Rails 7/8中has_one :through关联的counter_cache异常排查
问题描述
尝试在Company模型中实现基于BusinessUnit关联Location的counter_cache,但出现以下异常:
- 增删Location时,
locations_count计数器无法自动更新 - 将包含N个Location的BusinessUnit转移到其他公司时,计数器仅增减1而非N
- 手动调用
Company.reset_counters(1, :locations)可正确设置计数,该问题在Rails 7.1.3和8.0.1中均存在
原模型代码
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
问题原因
- Rails原生
counter_cache不支持has_many through关联,直接在Company的locations关联上配置无效 - BusinessUnit中
belongs_to :company, counter_cache: :locations_count配置错误——该配置会让Rails统计BusinessUnit的数量而非Location数量,导致转移单元时计数器仅变化1
解决方案
通过模型回调手动实现计数器更新,替代无效的原生counter_cache配置:
修改后的模型代码
Company模型
class Company < ApplicationRecord has_many :business_units, dependent: :destroy has_many :locations, through: :business_units, dependent: :destroy end
BusinessUnit模型
class BusinessUnit < ApplicationRecord belongs_to :company has_many :locations, dependent: :destroy # 业务单元转移公司时,调整原公司和新公司的Location计数 before_update :adjust_company_location_counts, if: :company_id_changed? private def adjust_company_location_counts location_count = locations.count # 给原公司减去当前单元的Location数量 Company.decrement_counter(:locations_count, company_id_was) if company_id_was.present? # 给新公司加上当前单元的Location数量 Company.increment_counter(:locations_count, company_id) if company_id.present? end end
Location模型
class Location < ApplicationRecord belongs_to :business_unit has_one :company, through: :business_unit # 创建Location时,对应公司计数器+1 after_create :increment_company_location_count # 删除Location时,对应公司计数器-1 after_destroy :decrement_company_location_count private def increment_company_location_count Company.increment_counter(:locations_count, company.id) if company.present? end def decrement_company_location_count Company.decrement_counter(:locations_count, company.id) if company.present? end end
关键说明
- 使用
increment_counter/decrement_counter直接操作数据库,避免并发场景下的计数错误 - BusinessUnit的
before_update回调处理单元转移时的批量计数调整,确保计数器变化值为单元下的Location总数 - Location的回调处理单个增删操作时的计数更新,覆盖所有Location变更场景
内容的提问来源于stack exchange,提问作者NGobin
相关产品推荐
相关产品推荐

