Sunday, September 5, 2010

Skipping Active Record Callbacks in Rails

It is often desirable to skip ActiveRecord callbacks defined on rails model objects. For a somewhat contrived(but true life) example, let's say we have an ActiveRecord::Observer that defines an after_save callback which can be called on the observed models. Sort of like "pseudo polymorphic callback inheritance"?


Updating, saving, validating, or performing other ActiveRecord operations on our model object inside the callback itself can cause stack overflows and other undesired behavior to occur, and it's not always obvious where the problem is occuring.


One elegant solution to avoiding this particular problem can be achieved by extending ActiveRecord::Observer with a skip_callback method. With the following code, we get a singleton method on our observer model. This method accepts a block of (almost) any arbitrary code. The method first stubs out our callback as a boolean, yields to our code, and then re-defines the callback method as originally defined:


config/initializers/active_record_observer_extensions.rb:


class ActiveRecord::Observer
  def self.skip_callback(callback, &block)
    method = instance_method(callback)
    remove_method(callback) if respond_to?(callback)
    define_method(callback){ true }
    yield
    remove_method(callback)
    define_method(callback, method)
  end
end

Where I am assuming your observer looks something like this:

app/models/fyle_observer.rb:

class FyleObserver < ActiveRecord::Observer
  observe :script, :document, :image

  def after_save(observed)
      ...assign upload attributes, etc...
      FyleObserver.skip_callback(:after_save) do
        Fyle.update_attributes(:mime_type => 'application/msword')
    end
  end

end

Code Explanation: If you tried to update Fyle inside the after_save method in FyleObserver without skipping the callback, your application would surely freeze up because you would trigger an endless chain of after_save callbacks(like standing between two mirrors and seeing infinity!) Here's what happens in the skip_callback method: First the callback is placed into the temp variable named method. Then the original callback named as a parameter is removed from the ActiveRecord::Observer class if it responds to it. Next it is defined as a simple method that returns true. Then the code comprising our block is evaluated before finally the simple method is removed and the original re-defined.

No comments:

Post a Comment