Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to disable the Rails association counter_cache at runtime

I am importing records in bulk and don't want to update the counter each time. I want to skip the counter_cache sql updates during the bulk upsert then call reset_counters at the end of the loop.

I have tried:

my_model = MyModel.find_or_initialize_by_slug row[:slug]
my_model.association(:my_association).reflection.options[:counter_cache] = false
my_model.attributes = {:name => "Christopher"}
my_model.save!

but I can see in the sql output that it's still updating the counter_cache.

note: I can't use activerecord-import because I want to perform upserts and I am using postgresql

like image 901
Christopher Manning Avatar asked Feb 16 '12 17:02

Christopher Manning


1 Answers

You have a few different options for skipping an update of the counter cache, and the one that you choose really depends on how you want to structure your application. I'll discuss the different ways that you can get around the counter cache and mention some of the considerations you may want to make in doing so.

Basically, there are three different ways you can skip the update of the counter cache:

  1. Monkey patch your model to disable the counter cache callback
  2. Use an update method that doesn't trigger callbacks
  3. Define an alternative model pointing to the same table that doesn't have the same callback behavior, and use that when bulk inserting into the database

Note that the first two options above are more generally related to disabling callbacks in ActiveRecord, and this makes sense because the counter cache is implemented internally by means of a callback.

When Rails loads a model that has associations with a counter cache, it dynamically defines callback methods. If you want to disable these as a callback you have to first figure out what the callback names are.

There are two primary ways of figuring out what the methods are that Rails has defined in order to implement these callbacks. You can read the Rails source to find out the names that it is going to generate through String intorpolation, or you can use introspection to figure out what methods your class responds to. I'll give an example of how you can use introspection to find out the callbacks defined by ActiveRecord in order to automatically implement counter caching.

Let's suppose you have a class called SpecialReply that descends from a Reply class that descends from ActiveRecord::Base (this example comes from the test suite with Rails). It has have a counter cache column as defined below:

class SpecialReply < ::Reply
  belongs_to :special_topic, :foreign_key => 'parent_id', :counter_cache => 'replies_count'
end

In the console, you can see what methods your class responds to by using .methods. This is going to produce a lot of noise, since every instance of Object already responds to a lot of methods, so you can narrow down the list as follows:

1.9.3-p194 :001 > sr = SpecialReply.new
1.9.3-p194 :002 > sr.methods - Object.methods

On the second line you're saying, show me all of the methods that my instance of SpecialReply responds to, minus the ones that all Objects respond to. This often helps with introspection by filtering out methods which aren't particular to the class type you're looking at.

Unfortunately even after this filtering there is a lot of noise due to the methods that ActiveRecord adds to all of its descendent classes. In this case grep is useful - since ActiveRecord helpfully creates counter callback methods containing the String counter_cache (see the meta-programming used by ActiveRecord to generate a counter cache method for a belongs_to association), you can find out the callbacks defined related to counter caches with the following:

1.9.3-p194 :001 > sr = SpecialReply.new
1.9.3-p194 :002 > sr.methods.map(&:to_s).grep(/counter_cache/)

Notice that since grep operates on a String, and methods returns an Array of symbol method names, we first use a to_proc (&:) in order to convert all Symbols to Strings and then grep out the ones containing counter_cache. That leaves me with the following methods that seem like they were probably auto-generated by ActiveRecord as callbacks for implementing counter caching:

belongs_to_counter_cache_after_create_for_special_topic
belongs_to_counter_cache_before_destroy_for_special_topic
belongs_to_counter_cache_after_create_for_topic
belongs_to_counter_cache_before_destroy_for_topic
belongs_to_counter_cache_after_create_for_topic_with_primary_key
belongs_to_counter_cache_before_destroy_for_topic_with_primary_key

You should be able to follow a similar process in your program to determine the method names added by ActiveRecord so that you can knock them out following existing instructions for removing callbacks.

Your choice from the above options really depends on the structure of your program, and the tradeoffs you are willing to consider for increased efficiency of loading data. Worthy of note is that the first two options can make your code less readable by modifying the behavior of a class from outside (monkey patching) and can make your system unstable by bypassing business rules (updates of the cache columns) on updates of data. For these reasons I would think about whether you could create another class for loading your data in an optimized way while minimizing impacts on readability or consistency of data.

like image 119
Justin Leitgeb Avatar answered Nov 15 '22 09:11

Justin Leitgeb