Active Record Callbacks

2018/11/15

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)并不在savetransaction中,所以如果after_commit触发了异常, 那么在rails3ActiveRecord::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

直接捕获了并没有再抛出…

这样就导致commitCallbackChain中异常后的其他的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_ordercallbacks,然后我们又发现在update_order这个方法中,调用了run_callbacks :update_order,这个方法的作用就是用来执行刚才定义过的:update_ordercallbacks. 但是在执行该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

其中startend分别是对:before:aftercallback解析,而:around则在前后分别都有解析.

rails4和rails3在callbacks实现上的异同

rail4rails3的这块的总体实现思路是一样的,都是将需要实现回调的代码块传入到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判断条件.


Show Disqus Comments