Disable ActiveRecord Callbacks

September 29th, 2008

Rails ActiveRecord callbacks are great. Callbacks give you "aspect-oriented" behavior that really helps clean up model code and isolate ancillary actions.

But sometimes those callbacks can get in the way. I am working on a large, data-intensive application that provides an automated way to import data from a legacy application into the Rails application. In some cases, when uploading large quantities of data, the callbacks can prove to be a performance hindrance because of the overhead involved.

It would be nice if it were possible to disable these callbacks programmatically. I use rake tasks to drive the legacy data import routines. If I had a way to tell an ActiveRecord model class to ignore callbacks, it would allow intensive import routines to run much faster.

Granted, this is inherently dangerous; Rails rightfully assumes that
callbacks are there for a purpose and (as far as I can tell) does not provide a way to disable them. But in my case, it made sense to provide such an ability.

My solution was to open up ActiveRecord::Base and use Ruby's metaprogramming tricks to selectively undefine (disable) and redefine (enable) a given callbacks. I placed the Ruby file containing this "monkey patch" in the lib folder of my Rails app.

module ActiveRecord
  class Base
    class << self
      def disable_callback(callback)
        if callback.to_s =~ /^(after|before)_/ && method_defined?(callback)
          alias_method "__#{callback}", callback
          undef_method callback
          true
        else
          false
        end
      end
      def enable_callback(callback)
        if callback.to_s =~ /^(after|before)_/ && 
           !method_defined?(callback) && 
           method_defined?("__#{callback}") 
          alias_method callback, "__#{callback}"
          undef_method "__#{callback}"
          true
        else
          false
        end
      end
    end
  end  
end

Let's take a what this code does. First of all, for those new to Ruby, Ruby supports open classes. In other words, a programmer can open any loaded class and change its implementation. The class that opened up is not replaced; rather, it is modified on-the-fly (when the monkey patch is loaded). In this case, I am opening up the Base class for ActiveRecord models. I want to add a class method to Base that allows me to disable and reenable callbacks. I would like the API to work as follows:

# Disable a given callback
MyModel.disable_callback(:after_save)

# Import the data (or whatever data-intensive operation is to be performed)
MyModel.import_data(...)

# Now reenable the callback
MyModel.enable_callback(:after_save)

Now, back to the implementation. I want to undefine the callback method when it's disabled, and redefine it what it's enabled. To accomplish this I can use Ruby's undef method. But what a second -- I want to add disable_callback and enable_callback as class methods; but I need them to undefine instance methods (e.g. after_save). How is this possible?

Ruby has the concept of an eigenclass (aka the "singleton class" and the "metaclass"). We can open up the eigenclass and then add methods that can affect created instances. (For an excellent explanation check out Seeing Metaclasses Clearly).

Using this technique, we can add class methods to any class that extends ActiveRecord::Base, and, within those class methods, modify the callback methods (which are instance methods). So to disable a callback, I first alias the method (so that it can be re-enabled) and then undefine it. To re-enable a callback method I reverse the process ... that is, I find the aliased (original) callback method, and re-alias it to its proper name.

In fact, you could use this approach to disable/enable any method on an ActiveRecord object. To prevent such encompassing behavior, I put some checks in place that ensure that the method name conforms to the naming convention for callback methods.

Well, this technique should be used carefully, I was pleased that I could coerce the behavior needed using Ruby's powerful metaprogramming capabilities.


Your Response