Skip rails counter_cache update

1.3k Views Asked by At

I have a model that uses rails' built-in counter_cache association to increment/decrement counts. I have a requirement wherein I need to disable this when I destroy the model for a specific situation. I have tried to do something like Model.skip_callback(:destroy, :belongs_to_counter_cache_after_update) but it doesn't seem to work as expected (i.e it still ends up decrementing the associated model). Any helpful pointers would be appreciated.

2

There are 2 best solutions below

1
Thanh On

You can create a flag to decide when callback should be run, something like:

class YourModel
  attr_accessor :skip_counter_cache_update

  def decrement_callback
    return if @skip_counter_cache_update
    # Run callback to decrement counter cache
    ...
  end  
end

so before you destroy your object of a Model, just set value for skip_counter_cache_update:

@object = YourModel.find(some_id)
@object.skip_counter_cache_update = true
@object.destroy

so it will not run decrement callback.

0
Zia On

One option is to temporarily override the method responsible for updating the cache count in case of destroy. For example if you have following two models

class Category < ActiveRecord::Base
  has_many :products
end

class Product < ActiveRecord::Base
  belongs_to :category, counter_cache: true
end

Now you can try to find the methods responsible for updating cache count with following

2.1.5 :038 > Product.new.methods.map(&:to_s).grep(/counter_cache/)

This shows all the product instance methods which are related to counter_cache, with following results

=> ["belongs_to_counter_cache_before_destroy_for_category", "belongs_to_counter_cache_after_create_for_category", "belongs_to_counter_cache_after_update_for_category"]

From the names of the methods it shows that

"belongs_to_counter_cache_after_create_for_category"

might be responsible for counter cache update after destroy. So I decided to temporarily override this method with one fake method which doesn't do anything(to skip counter cache update)

Product.class_eval do
  def fake_belongs_to_counter_cache_before_destroy_for_category; end
  alias_method :real_belongs_to_counter_cache_before_destroy_for_category, :belongs_to_counter_cache_before_destroy_for_category
  alias_method :belongs_to_counter_cache_before_destroy_for_category, :fake_belongs_to_counter_cache_before_destroy_for_category
end

Now if you will destroy any product object, it will not update counter cache in Category table. But its very important to restore the actual method after you have run your code to destroy specific objects. To restore to actual class methods you can do following

Product.class_eval do
  alias_method :belongs_to_counter_cache_before_destroy_for_category, :real_belongs_to_counter_cache_before_destroy_for_category
  remove_method :real_belongs_to_counter_cache_before_destroy_for_category
  remove_method :fake_belongs_to_counter_cache_before_destroy_for_category
end

To ensure that the methods definitions always restored after your specific destroy tasks, you can write a class method, that will make sure to run both override and restore code

class Product < ActiveRecord::Base
  belongs_to :category, counter_cache: true

  def self.without_counter_cache_update_on_destroy(&block)
    self.class_eval do
      def fake_belongs_to_counter_cache_before_destroy_for_category; end
        alias_method :real_belongs_to_counter_cache_before_destroy_for_category, :belongs_to_counter_cache_before_destroy_for_category
        alias_method :belongs_to_counter_cache_before_destroy_for_category, :fake_belongs_to_counter_cache_before_destroy_for_category
    end
    yield
    self.class_eval do
      alias_method :belongs_to_counter_cache_before_destroy_for_category, :real_belongs_to_counter_cache_before_destroy_for_category
      remove_method :real_belongs_to_counter_cache_before_destroy_for_category
      remove_method :fake_belongs_to_counter_cache_before_destroy_for_category
    end
  end
end

Now if you destroy any product object as given following

Product.without_counter_cache_update_on_destroy { Product.last.destroy }

it will not update the counter cache in Category table.

References:
Disabling ActiveModel callbacks https://jeffkreeftmeijer.com/2010/disabling-activemodel-callbacks/ Temporary overriding methods: https://gist.github.com/aeden/1069124