Skip to content

Instantly share code, notes, and snippets.

@RStankov
Created August 24, 2022 12:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RStankov/24241d6892eeed49ae435a2f549be999 to your computer and use it in GitHub Desktop.
Save RStankov/24241d6892eeed49ae435a2f549be999 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
# Complex counter counter caches
#
# Usage:
#
# In parent model
# class Post < ApplicationRecord
# ExplicitCounterCache.define(self, votes_count, -> { votes.active })
# ExplicitCounterCache.define(self, comments_count, -> { votes.visible })
# end
#
# In children models
# class Vote < ApplicationRecord
# ExplicitCounterCache::Refresh.define(self, :post, votes_count)
# end
#
# class Comment < ApplicationRecord
# ExplicitCounterCache::Refresh.define(self, :comment, votes_count)
# end
#
module ExplicitCounterCache
def self.define(model, counter_name, fetch_relation)
model.define_method "refresh_#{counter_name}" do
relation_or_count = instance_exec(&fetch_relation)
new_count = relation_or_count.is_a?(Numeric) ? relation_or_count : relation_or_count.count
return if self[counter_name] == new_count
return if destroyed?
# We intentionally avoid callbacks/validations here
# since these might be too expensive in some cases, e.g. checking
# another field for uniqueness even though we're just updating a counter.
update_columns counter_name => new_count, updated_at: Time.current
end
end
module Refresh
def self.define(model, parent_model, counter_name, same_transaction: false)
method_name = "refresh_#{parent_model}_#{counter_name}".to_sym
model.define_method method_name do
public_send(parent_model)&.public_send("refresh_#{counter_name}")
end
model.instance_eval do
if same_transaction
after_create method_name
after_destroy method_name
else
after_commit method_name
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment