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