2

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.

db_db
  • 21
  • 2
  • If you are ok to skip all callbacks, you can use `Model.first.delete` – Md. Farhan Memon Jun 10 '17 at 03:29
  • Yeah I'm aware of methods to skip callbacks but I have a specific need to disable just the counter callback. For now, I'm using `destroy_all` and then reset the counter (which seems wasteful). – db_db Jun 11 '17 at 01:15

2 Answers2

2

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

Zia
  • 23
  • 5
1

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.

Thanh
  • 8,219
  • 5
  • 33
  • 56
  • 1
    The callback to decrement counter cache is built into rails and I want to disable that instead of creating my own counter methods. – db_db Jun 12 '17 at 19:02