ActiveSupport中的callbacks
阅读本文,您将看到:
- ActiveRecord::Base#save的流程
- callbacks的定义与基本原理
- callbacks在rails3中的大致实现流程
- rails4和rails3在callbacks实现上的异同
- 日常使用中有哪些可能需要注意的细节
如果只是想了解ActiveRecord::Base#save时的callbacks调用流程,只看第一部分即可,之后是ActiveSupport::Callbacks的一些分析.
ActiveRecord::Base.save的流程
save的实现流程
在开始说callbacks之前,先介绍下平时调用ActiveRecord::Base#save的时候都经过了哪些callbacks流程:
def save
vaild_result = \
run_callbacks :validation do
puts 'validate sth'
true
end
if vaild_result
ActiveRecord::Base.transaction do
run_callbacks :save do
create_or_update
end
end
begin
committed
rescue Exception => ex
Rails.logger.error(ex.inspect)
end
true
else
false
end
end
def committed
run_callbacks :commit do
puts 'commit balabala...'
end
end
def create_or_update_stub
new_record? ? create : update
end
def create
run_callbacks :create do
puts 'INSERT INTO test_news balabala...'
end
end
def update
run_callbacks :update do
puts 'update test_news balabala...'
end
end
ActiveRecord::Callbacks的封装方式
ActiveRecord::Callbacks实际上是在ActitveSupport::Callbacks
的基础上封装了一些如save,commit,validation这样的回调,在我们调用如
after_commit :test
的时候,其实调用的是:
set_callback :commit, :after, :test
完成了set_callback的过程.
当然,在我们调用after_commit之前,首先会声明这个callbacks,例如
define_callbacks :save
就定义了:commit这个callback.
而run_callbacks的方法则隐藏在对save,commit等方法的封装中.
调用save的时候callbacks的执行流程
- (-) save
- (-) valid
- (1) before_validation
- (-) validate
- (2) after_validation
- (3) before_save
- (4) before_create
- (-) create
- (5) after_create
- (6) after_save
- (7) after_commit
其中(3)~(6)是在一个transaction里的.
整个回调链包含在一个事务中。如果任何一个 before_* 回调方法返回 false 或抛出异常,整个回调链都会终止执行,撤销事务;而 after_* 回调只有抛出异常才能达到相同的效果。
调用after_commit时抛出异常会怎么样
由于after_commit(以及after_rollback)并不在save的transaction中,所以如果after_commit触发了异常,
那么在rails3的ActiveRecord::Transaction中,有如下代码:
# Call the after_commit callbacks
def committed! #:nodoc:
run_callbacks :commit
ensure
clear_transaction_record_state
end
从这里看异常是被抛出来了,然而调用committed!的地方:
# Send a commit message to all records after they have been committed.
def commit_transaction_records
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
unless records.blank?
records.uniq.each do |record|
begin
record.committed!
rescue Exception => e
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
end
end
直接捕获了并没有再抛出…
这样就导致commit的CallbackChain中异常后的其他的Callback都没有执行,并且除了系统日志外基本没有什么痕迹留下来,出了bug基本没法查.
正在考虑在这个地方加一个日志,将after_commit的异常存起来,以供bug查询用.
callbacks的定义与基本原理
定义
在rails的源码注释中,对于callbacks的基本定义是在对象的生命周期中的某些关键点执行的一些钩子代码.
基本原理
如果我们要定义并一个callbacks,基本步骤有三个,分别对应三个ActiveSupport::Callbacks中的三个基本方法:
-
define_callbacks 这个方法的主要作用是定义一个callbacks
我们来看一个只include了
ActiveSupport::Callbacks的例子:
class Record
include ActiveSupport::Callbacks
define_callbacks :update_order
def update_order
run_callbacks :update_order do
puts "- update_order"
end
end
end
在上面的代码中我们定义了一个叫做:update_order的callbacks,然后我们又发现在update_order这个方法中,调用了run_callbacks :update_order,这个方法的作用就是用来执行刚才定义过的:update_order的callbacks. 但是在执行该callbacks的时候,我们发现我们只是定义了这个callbacks,并没有告诉程序在这个callbacks中我们要执行什么,于是就有了下面这个方法:
set_callback :update_order, :before, :test1
完整代码应该是这样子:
class Record
include ActiveSupport::Callbacks
define_callbacks :update_order
set_callback :update_order, :before, :test1
def update_order
run_callbacks :update_order do
puts "- update_order"
end
end
def test1
puts 'this is a callback.'
end
end
set_callback这个方法将我们定义的需要在:update_order中执行的代码块传入了这个callbacks中,现在执行的结果如下:
a = Record.new
<Record:0x007ff7c0615d18>
a.update_order
this is a callback.
- update_order
我们看到,一个callbacks需要首先经过声明定义(define_callbacks),然后设置(set_callback),最后在需要的地方执行(run_callbacks),这样才是一个完整有意义的callbacks.
callbacks在rails3中的大致实现流程
define_callbacks
源码如下:
def define_callbacks(*callbacks)
config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
callbacks.each do |callback|
class_attribute "_#{callback}_callbacks"
send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
__define_runner(callback)
end
end
*callbacks这个参数里是需要声明的callbacks的名字,例如:update_order,然后给每个callbacks声明了set,get和runner(执行方法).
这里可以看到_update_order_callbacks本身是一个CallbackChain,这是一个队列,存放了每个需要执行的代码块的信息.
这里要说明的是 __define_runner 这个方法,源码如下:
# Generate the internal runner method called by +run_callbacks+.
def __define_runner(symbol) #:nodoc:
runner_method = "_run_#{symbol}_callbacks"
unless private_method_defined?(runner_method)
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{runner_method}(key = nil, &blk)
self.class.__run_callback(key, :#{symbol}, self, &blk)
end
private :#{runner_method}
RUBY_EVAL
end
end
这个方法定义了一个叫做_run_update_order_callbacks的方法,run_callbacks执行的时候就是在调用这个方法,到这里,define_callbacks的工作就做完了.
set_callback
定义了_update_order_callbacks这个CallbackChain之后,这个队列是空的,如果不掉用set_callback方法加入callbacks需要执行的代码块的信息,那么运行runner_callbacks的时候就相当于没有执行任何有效代码.
def set_callback(name, *filter_list, &block)
mapped = nil
__update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
mapped ||= filters.map do |filter|
Callback.new(chain, filter, type, options.dup, self)
end
filters.each do |filter|
chain.delete_if {|c| c.matches?(type, filter) }
end
options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped)
target.send("_#{name}_callbacks=", chain)
end
end
run_callbacks
这个方法是执行callbacks的主要方法,但是主要实现并不在该方法中,该方法源码如下:
def run_callbacks(kind, *args, &block)
send("_run_#{kind}_callbacks", *args, &block)
end
其中_run_#{kind}_callbacks这个方法之前在调用define_callbacks时已经声明过了,是这个样子:
def _run_#{symbol}_callbacks(key = nil, &blk)
self.class.__run_callback(key, :#{symbol}, self, &blk)
end
private :#{_run_#{symbol}_callbacks}
其中有个类方法__run_callback,这个方法才是执行时的主要方法:
# This method calls the callback method for the given key.
# If this called first time it creates a new callback method for the key,
# calculating which callbacks can be omitted because of per_key conditions.
#
def __run_callback(key, kind, object, &blk) #:nodoc:
name = __callback_runner_name(key, kind)
unless object.respond_to?(name, true)
str = object.send("_#{kind}_callbacks").compile(key, object)
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{name}() #{str} end
protected :#{name}
RUBY_EVAL
end
object.send(name, &blk)
end
其中compile方法的作用是根据CallbackChain中的各个callback生成一个可执行的完整的包含原方法逻辑的方法(的string),然后通过send来调用这个生成的方法,整个回调的执行过程就完成了,compile的源码:
def compile(key=nil, object=nil)
method = []
method << "value = nil"
method << "halted = false"
each do |callback|
method << callback.start(key, object)
end
if config[:rescuable]
method << "rescued_error = nil"
method << "begin"
end
method << "value = yield if block_given? && !halted"
if config[:rescuable]
method << "rescue Exception => e"
method << "rescued_error = e"
method << "end"
end
reverse_each do |callback|
method << callback.end(key, object)
end
method << "raise rescued_error if rescued_error" if config[:rescuable]
method << "halted ? false : (block_given? ? value : true)"
method.compact.join("\n")
end
其中start和end分别是对:before和:after的callback解析,而:around则在前后分别都有解析.
rails4和rails3在callbacks实现上的异同
rail4和rails3的这块的总体实现思路是一样的,都是将需要实现回调的代码块传入到run_callbacks方法中,然后插入到声明过的callbacks中间执行,但是rails4用了更好的封装来实现,并且从设计上改进了一些问题,比如说加了线程锁,每次调用run_callbacks时实质上都是根据判断执行相应的代码块,避免了用eval这种方式拼接方法带来的可能出现的问题.
主要实现方式不同的地方有:
def __run_callbacks__(callbacks, &block)
if callbacks.empty?
yield if block_given?
else
runner = callbacks.compile
e = Filters::Environment.new(self, false, nil, block)
runner.call(e).value
end
end
其中的runner是在compile方法中得到了一个CallbackSequence的对象,这个对象有@before和@after两个实例变量,都是Array,里面存放了需要执行的很多proc,调用call方法的时候会先调用@before里的每个proc,再执行方法本身的代码块,再执行@after中的proc,完成整个回调.
def compile
@callbacks || @mutex.synchronize do
final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) }
@callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
callback.apply callback_sequence
end
end
end
其中的apply方法:
def apply(callback_sequence)
user_conditions = conditions_lambdas
user_callback = make_lambda @filter
case kind
when :before
Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter)
when :after
Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config)
when :around
Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config)
end
end
这里的user_coditions是根据传入参数生成的判断是否执行的代码块,而make_lambda则是根据传入的filter类型的不同生成了相应的执行代码块,最后通过相应的build方法生成真正执行的代码块,最后放到传入的callback_sequence中.
以上列举了rails4代码和rails3代码的一些区别,而最新的rails v5.0.0.1在这块的的实现与rails4只有一些细节上的修改,但是在rails5的最后发现了一段warning:
Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in Rails 5.1.
To explicitly halt the callback chain, please use `throw :abort` instead.
所以5.1看起来会很大程度修改这一块的condition判断条件.