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
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.
Bill's Tweeting His iPOD A to Z!
July 11th, 2008
Rails 2.1 Migrations - Looking out for your best interests
June 9th, 2008
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.

