跟踪一下touch

[3] pry(main)> binding.trace_tree(htmp: 'ar/touch'){ ss.touch }


调用栈如下

20171106_221414_551_touch.html

首先是经过module NoTouching。no_touching会将接收消息AR类入栈Thread.current[:no_touching_classes] ||= [],然后block中的touch会检查被touch的AR类是否在栈中有货有父类,有则不touch。注释如下,已解释得很清楚(不过还少了入栈出栈所产生的作用域)

# ActiveRecord::Base.no_touching do
#   Project.first.touch  # does nothing
#   Message.first.touch  # does nothing
# end
#
# Project.no_touching do
#   Project.first.touch  # does nothing
#   Message.first.touch  # works, but does not touch the associated project
# end


接着,便到了callback,rails默认只提供after_touch来定义

# activerecord-5.1.2/lib/active_record/callbacks.rb
def touch(*)
  _run_touch_callbacks { super }
end


最核心的操作,便是activerecord-5.1.2/lib/active_record/persistence.rb

def touch(*names, time: nil)
  unless persisted?
    raise ActiveRecordError, <<-MSG.squish
      cannot touch on a new or destroyed record object. Consider using
      persisted?, new_record?, or destroyed? before touching
    MSG
  end

  time ||= current_time_from_proper_timezone
  attributes = timestamp_attributes_for_update_in_model
  attributes.concat(names)

  unless attributes.empty?
    changes = {}

    attributes.each do |column|
      column = column.to_s
      changes[column] = write_attribute(column, time)
    end

    primary_key = self.class.primary_key
    scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key))

    if locking_enabled?
      locking_column = self.class.locking_column
      scope = scope.where(locking_column => _read_attribute(locking_column))
      changes[locking_column] = increment_lock
    end

    clear_attribute_changes(changes.keys)
    result = scope.update_all(changes) == 1

    if !result && locking_enabled?
      raise ActiveRecord::StaleObjectError.new(self, "touch")
    end

    @_trigger_update_callback = result
    result
  else
    true
  end
end


要点如下:

更新为当前时间或用:time选项来指定

被更新的栏位默认有timestamp_attributes_for_update_in_model,即["updated_at", "updated_on"],也可调用touch时加上其他栏位

更新操作使用的是update_all,所以此处没有调任何callback,只有after_touch和transation的after_commit/rollback

被更新的栏位不记录到dirty中

而belongs_to的:touch选项,猜测是用callback来实现,可以跟踪看看

class Spirit < ApplicationRecord
  binding.trace_tree(htmp: 'ar/belongs_to_touch'){ belongs_to :human, touch: true }
end


调用栈如下

20171107_214256_080_belongs_to_touch.html

源码可追溯至

def self.add_touch_callbacks(model, reflection)
  foreign_key = reflection.foreign_key
  n           = reflection.name
  touch       = reflection.options[:touch]

  callback = lambda { |changes_method| lambda { |record|
    BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
  }}

  model.after_save    callback.(:saved_changes), if: :saved_changes?
  model.after_touch   callback.(:changes_to_save)
  model.after_destroy callback.(:changes_to_save)
end


即是加了:touch后,子表的save、touch、destroy都会touch父表