Jeff Kreeftmeijer

Disabling ActiveModel callbacks

2010-09-13

Remember the last time you wanted to create, update, save or destroy a record or document and your before_create or after_update fired when you didn’t want it to? Or are you using Mongoid and did you include Mongoid::Timestamps but don’t want your updated_at attribute to change for a specific action?

Think for a second. Adding callbacks to disable them later is nasty and will result in code that’s more difficult to maintain. You probably made a wrong design decision here, so get back to the drawing board and rethink this part of your application if you can.

If you can’t or you’re completely sure you have a valid reason to skip your callbacks, you probably tried something like removing the callback method but found out that didn’t work anymore.

The way callbacks are handled completely changed in Rails 3, breaking approaches like above. The new skip_callback and set_callback can be of some help though:

class User
  # I'm using Mongoid, but this should work for anything based on
  # ActiveModel.
  include Mongoid::Document
  include Mongoid::Timestamps

  def sneaky_update(attributes)
    User.skip_callback(:save, :before, :set_updated_at)
    User.update_attributes(attributes)
    User.set_callback(:save, :before, :set_updated_at)
  end

end

This will keep Mongoid::Timestamps from calling set_updated_at before saving the Document.

Because I didn’t like how this looked, I added a method called without_callback to ActiveSupport::Callbacks to allow you to temporarily disable callbacks in a block. Just throw this in config/initializers/without_callback.rb:

module ActiveSupport::Callbacks::ClassMethods
  def without_callback(*args, &block)
    skip_callback(*args)
    yield
    set_callback(*args)
  end
end

And you’ll be able to do stuff like this:

class User
  # I'm using Mongoid, but this should work for anything based on
  # ActiveModel.
  include Mongoid::Document
  include Mongoid::Timestamps

  def sneaky_update(attributes)
    User.without_callback(:save, :before, :set_updated_at) do
      update_attributes(attributes)
    end
  end

end

At least, that’s how I solved it. I’m not completely sure this is the prettiest way to do something like this. Do you know of a better way? Be sure to let me know in the comments.