Module: ConcernsOnRails::Models::CounterCacheable

Extended by:
ActiveSupport::Concern
Defined in:
lib/concerns_on_rails/models/counter_cacheable.rb

Overview

Conditional, denormalized association counters (“counter_culture-lite”). Rails’ native ‘belongs_to …, counter_cache: true` maintains exactly one column counting every child — it cannot keep an `approved_comments_count` next to a `comments_count`, and it has no way to repair drift after a backfill or a counter_cache-less write. This concern, declared on the CHILD, keeps one or many parent columns in sync, each with an optional `if:` condition, and ships a `recount_counter_caches!` repair method.

class Comment < ApplicationRecord
  include ConcernsOnRails::CounterCacheable
  belongs_to :post                       # declare the belongs_to FIRST
  belongs_to :author, class_name: "User"

  counter_cacheable_by :post                          # posts.comments_count
  counter_cacheable_by :post, count: :approved_comments_count,
                              if: -> { approved? }    # conditional counter
  counter_cacheable_by :author, count: :posts_count, touch: true
end

Post.find(1).comments_count            # maintained on create/destroy/update
Comment.recount_counter_caches!        # repair/backfill every counter

Behaviour:

* create/destroy adjust the counter by ±1 when the foreign key is present
  and the `if:` condition holds for the record's current state.
* update handles BOTH a foreign-key reparent (the row moved to another
  parent) AND a condition flip (the `if:` result changed): the old parent
  is decremented if it used to count the row, the new parent incremented
  if it counts it now. A no-op save writes nothing.
* Adjustments use `update_counters` — a single SQL `COALESCE(col,0) ± 1`,
  atomic under concurrency — and run inside the record's own save
  transaction, so a rolled-back save rolls back the counter too.

Notes:

* The `belongs_to` must be declared BEFORE the macro (the reflection is
  validated at declaration time). Polymorphic associations are not
  supported in this version.
* Do NOT also set native `counter_cache: true` on the same column — both
  would fire and double-count.
* Counters track the PERSISTED record. Writes that skip callbacks
  (`update_column(s)`, `update_all`, `delete`, raw SQL) are not tracked —
  run `recount_counter_caches!` to reconcile.
* `if:` conditions should read the record's OWN columns; the previous
  state is reconstructed from the changed attributes, not the
  associations.
* `recount_counter_caches!` rewrites every parent's counter and, for a
  conditional counter, scans the children in Ruby (portable across
  adapters, but O(n)) — a maintenance operation, run it offline.
* Reach for the `counter_culture` gem when you need multi-level rollups,
  delta columns, or after-commit execution.

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

LABEL =
"ConcernsOnRails::Models::CounterCacheable".freeze