6

Is it possible to add a callback to a single ActiveRecord instance? As a further constraint this is to go on a library so I don't have control over the class (except to monkey-patch it).

This is more or less what I want to do:

def do_something_creazy
  message = Message.new
  message.on_save_call :do_even_more_crazy_stuff
end

def do_even_more_crazy_stuff(message)
  puts "Message #{message} has been saved! Hallelujah!"
end

5 Answers 5

6

You could do something like that by adding a callback to the object right after creating it and like you said, monkey-patching the default AR before_save method:

def do_something_ballsy
    msg = Message.new
    def msg.before_save(msg)
        puts "Message #{msg} is saved."
        # Calls before_save defined in the model
        super
    end
end
Sign up to request clarification or add additional context in comments.

3 Comments

What about if there's already a before_save? Should I somehow save it and call it from my own before_save?
Edited my post for this one. Check it out.
Instead of defining the method on the objects metaclass, extend the object with a module. See my answer below. This allows you to 'stack' callbacks, by extending the object with multiple modules.
3

For something like this you can always define your own crazy handlers:

class Something < ActiveRecord::Base
  before_save :run_before_save_callbacks

  def before_save(&block)
    @before_save_callbacks ||= [ ]
    @before_save_callbacks << block
  end

protected
  def run_before_save_callbacks
    return unless @before_save_callbacks

    @before_save_callbacks.each do |callback|
      callback.call
    end
  end
end

This could be made more generic, or an ActiveRecord::Base extension, whatever suits your problem scope. Using it should be easy:

something = Something.new

something.before_save do
  Rails.logger.warn("I'm saving!")
end

2 Comments

Note that he doesn't access to change the class directly.
He did say "except to monkey patch it" which is precisely what this does if you apply it to ActiveRecord::Base directly.
2

I wanted to use this approach in my own project to be able to inject additional actions into the 'save' action of a model from my controller layer. I took Tadman's answer a stage further and created a module that can be injected into active model classes:

module InstanceCallbacks 
  extend ActiveSupport::Concern

  CALLBACKS = [:before_validation, :after_validation, :before_save, :before_create, :after_create, :after_save, :after_commit]

  included do
    CALLBACKS.each do |callback|
      class_eval <<-RUBY, __FILE__, __LINE__
        #{callback} :run_#{callback}_instance_callbacks

        def run_#{callback}_instance_callbacks
          return unless @instance_#{callback}_callbacks
          @instance_#{callback}_callbacks.each do |callback|
            callback.call
          end
        end

        def #{callback}(&callback)
          @instance_#{callback}_callbacks ||= []
          @instance_#{callback}_callbacks << callback
        end
      RUBY
    end
  end
end

This allows you to inject a full set of instance callbacks into any model just by including the module. In this case:

class Message
  include InstanceCallbacks
end

And then you can do things like:

m = Message.new
m.after_save do
  puts "In after_save callback"
end
m.save!

Comments

1

To add to bobthabuilda's answer - instead of defining the method on the objects metaclass, extend the object with a module:

def do_something_ballsy
  callback = Module.new do   
    def before_save(msg)
      puts "Message #{msg} is saved."
      # Calls before_save defined in the model
      super
    end
  end
  msg = Message.new
  msg.extend(callback)
end

This way, you can define multiple callbacks, and they will be executed in the opposite order you added them.

Comments

1

The following will allow you to use an ordinary before_save construction, i.e. calling it on the class, only in this case, you call it on the instance's metaclass so that no other instances of Message shall be affected. (Tested in Ruby 1.9, Rails 3.13)

msg = Message.new
class << msg
  before_save -> { puts "Message #{self} is saved" } # Here, `self` is the msg instance
end
Message.before_save # Calling this with no args will ensure that it gets added to the callbacks chain (but only for your instance)

Test it thus:

msg.save         # will run the before_save callback above
Message.new.save # will NOT run the before_save callback above

1 Comment

Problem is that after you do this, when you load and attempt to save the next model of the same class, you get an error like this: NoMethodError: undefined method '_callback_after_7872' for #<Message:0x0000000ff71980>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.