You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
问题原因
  1. Rails原生counter_cache不支持has_many through关联,直接在Company的locations关联上配置无效
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 13:13:17