先跟踪一下has_paper_trail方法是干什么的

[1] pry(main)> Student
=> Student (call 'Student.connection' to establish a connection)
[2] pry(main)> class Student
[2] pry(main)*   binding.trace_tree(htmp: 'paper_trail/has'){ has_paper_trail }
[2] pry(main)* end  
=> []


调用栈如下

20171004_220317_613_has.html

可见其主要就是让model类include了PaperTrail::Model::InstanceMethods,然后再加一些callback,拦截model的增删改,骨干如下



源码如下

# Set up `@model_class` for PaperTrail. Installs callbacks, associations,
# "class attributes", instance methods, and more.
# @api private
def setup(options = {})
  options[:on] ||= %i[create update destroy]
  options[:on] = Array(options[:on]) # Support single symbol
  @model_class.send :include, ::PaperTrail::Model::InstanceMethods
  if ::ActiveRecord::VERSION::STRING < "4.2"
    ::ActiveSupport::Deprecation.warn(
      "Your version of ActiveRecord (< 4.2) has reached EOL. PaperTrail " \
      "will soon drop support. Please upgrade ActiveRecord ASAP."
    )
    @model_class.send :extend, AttributeSerializers::LegacyActiveRecordShim
  end
  setup_options(options)
  setup_associations(options)
  setup_transaction_callbacks
  setup_callbacks_from_options options[:on]
  setup_callbacks_for_habtm options[:join_tables]
end


其中setup_options是对options进行“整理”和指定默认值。如果仅仅调用has_paper_trail,没给什么选项,那么默认就是这样

[14] pry(main)> Student.paper_trail_options
=> {:on=>[:create, :update, :destroy], :ignore=>[], :skip=>[], :only=>[], :meta=>{}, :save_changes=>true}


setup_associations是建立versions关联,使能通过student.versions查到过往版本。关联的名字可以通过:versions选项指定,但一般不建议改

setup_transaction_callbacks是用于重置“关联对象版本控制”的transaction_id的,详细来说:PaperTrail can restore three types of associations: Has-One, Has-Many, and Has-Many-Through. The transaction_id is a unique id for version records created in the same transaction. It is used to associate the version of the model and the version of the association that are created in the same transaction。不过,“关联对象版本控制”这一功能不太完美(详见官方文档),不推荐使用,就不深究了

setup_callbacks_for_habtm options也是,暂不深究

而setup_callbacks_from_options options,正是版本记录的功能所在,其源码如下。如果调用has_paper_trail时没有指定忽略增删改哪个事件,那么三个on*都会设置

def setup_callbacks_from_options(options_on = [])
  options_on.each do |event|
    public_send("on_#{event}")
  end
end


# Adds a callback that records a version after a "create" event.
def on_create
  @model_class.after_create { |r|
    r.paper_trail.record_create if r.paper_trail.save_version?
  }
  return if @model_class.paper_trail_options[:on].include?(:create)
  @model_class.paper_trail_options[:on] << :create
end


# Adds a callback that records a version before or after a "destroy" event.
def on_destroy(recording_order = "before")
  unless %w[after before].include?(recording_order.to_s)
    raise ArgumentError, 'recording order can only be "after" or "before"'
  end

  if recording_order.to_s == "after" && cannot_record_after_destroy?
    ::ActiveSupport::Deprecation.warn(E_CANNOT_RECORD_AFTER_DESTROY)
  end

  @model_class.send(
    "#{recording_order}_destroy",
    ->(r) { r.paper_trail.record_destroy if r.paper_trail.save_version? }
  )

  return if @model_class.paper_trail_options[:on].include?(:destroy)
  @model_class.paper_trail_options[:on] << :destroy
end


# Adds a callback that records a version after an "update" event.
def on_update
  @model_class.before_save(on: :update) { |r|
    r.paper_trail.reset_timestamp_attrs_for_update_if_needed
  }
  @model_class.after_update { |r|
    r.paper_trail.record_update(nil) if r.paper_trail.save_version?
  }
  @model_class.after_update { |r|
    r.paper_trail.clear_version_instance
  }
  return if @model_class.paper_trail_options[:on].include?(:update)
  @model_class.paper_trail_options[:on] << :update
end