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.

Ruby Range Intersection

August 29th, 2008

Here's an easy way to intersect two ranges in Ruby. The method will return a range for the range that is common (the overlap) between the two ranges; or nil if there is no overlap.
class Range
  def intersection(range)
    res = self.to_a & range.to_a
    res.empty? ? nil : (res.first..res.last) 
  end
  alias_method :&, :intersection
end

Waking Up This Morning

July 13th, 2008

Once upon a time I wrote a book. It was hard work, it was frustrating, it was fun, and it was rewarding. Since then I have done very little writing. Now is the time for that to change.

I slept in a bit today. The warm shower and the hot coffee called to me “You need us, you need us!”, and I did. Before embarking on my morning ritual of becoming lucid, I said to my son, “Good morning, son!” He’s eight. There are times when raising a child that they say the simplest little things that make you love them so much. All my son said to me was “Okay. I am going to feed all the animals, Dad.” That might not sound like a big thing, but we have a lot of animals.

There’s the 8-year old dog, Precious. Born roughly 3 months before the eight-year old son. Sweet, but a bit of a pain. She seems to enjoy the things that irritate us—incessantly chewing on her backside, and barking up a storm after she was let outside a minute before. Then there’s stealing your lunch as it sits on unguarded, and, of course, laying on the couch. With the latter she very consistently favors the love seat, deftly tossing off the throw pillows. She seems to know that they are throw pillows.

There are the two black cats, Pepper and Gulliver. Both sweet and both, well, annoying in their own special ways. Pepper, a.k.a. the “fat cat”, enjoys meowing and meowing until he’s fed. Two minutes later, the meowing begins again. Gulliver, on the other hand is much less vocal than his so-called brother. He makes up for this by annoying us in other ways—hiding in places that we still have no idea of where they are, and tossing up the occasional hair-ball. But, the one thing about Gulliver that sets him apart from the rest of the entire family, humanoids included, is that he has been with me for somewhere around 16 years. Long before I even had a family.

So we’ve got the dog and the two cats—now onto the more interesting critters. My son’s favorite pet, Crush, is a 3-year old Russian Tortoise. A tortoise, mind you, not a turtle. But it seems like he’s always being referred to as such. Whenever he hears “feed the turtle” he thinks to himself, “Stupid humans, I am going to start calling them ‘gorillas’!” When it comes to maintenance, Crush is the best of the bunch. Lettuce in the morning, a change of water every week, and a change of bedding every few months. If only all the other members of the household were so easy to care for! But, sad to say, we “stupid humans” still tend to neglect this poor creature. We let him walk around outside of his dry aquarium far too infrequently.

Okay, so we’re up to 4 animals—10 to go! The last 10 are actually the newest addition to the family. They’re all tropical fish, and they appear to be quite hardy considering we tend to neglect them as well.

So when my son said “I’ll feed all the animals” it was no small task. And the thought that he wanted to do this warmed my heart.

Check it out!
I hit upon a nice new feature of Rails 2.1 (at least, I believe this was introduced in 2.1). I was running my test suite in one terminal window, and in another, I went ahead and created a needed migration. I then went back to the window running the tests, identified and fixed some bugs, and then re-ran rake test:units whereupon I was greeted with this helpful message.
~/dev/projects/resman_machine $ rake test:units
(in /Users/bsiggelkow/dev/projects/foo_bar)
You have 1 pending migrations:
  20080609112303_AddFooToBars
Run "rake db:migrate" to update your database then try again.
This was definitely a problem that I run into numerous times before. Tests would fail simply because I had neglected to run some migrations. Rails just keeps on getting better and better.

Jelly Day

May 21st, 2008

I am excited about attending my first Jelly today. This particular Jelly was featured on CNN. NPR also did a story on Jellies a few months back as well.

Welcome!

May 19th, 2008

Welcome to the Camp ... I guess you all know why you are here.