Add save callback to a single ActiveRecord instance, is it possible?
Is it poss开发者_StackOverflow中文版ible 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
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
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
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!
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.
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
精彩评论