Rails ActiveRecord Object Instantiate

hooopo · 2015年01月04日 · 最后由 hooopo 回复于 2016年09月20日 · 4173 次阅读

initialize

最常用的就是使用new方法初始化一个AR对象,比如 User.new(:name => "user name"). new 方法通过 initialize 方法构造出一个新对象。大家几乎天天在用这个方法,不多说了。

# https://github.com/rails/rails/blob/f916aa247bddba0c58c50822886bc29e8556df76/activerecord/lib/active_record/core.rb#L276-L297

def initialize(attributes = nil, options = {})
  @attributes = self.class._default_attributes.dup

  init_internals
  initialize_internals_callback

  self.class.define_attribute_methods
  # +options+ argument is only needed to make protected_attributes gem easier to hook.
  # Remove it when we drop support to this gem.
  init_attributes(attributes, options) if attributes

  yield self if block_given?
  _run_initialize_callbacks
end

allocate + init_with

new 方法构造出来的对象在你未 save 之前都是 new_record,即:

user.new_record? #=> true

在一些场景,我们已经有了对象id和 Raw Attributes,我们想要初始化一个 AR 对象。最经典的场景就是从缓存读出 Raw Attributes 之后。这时,使用 new 方法就不合适。还好,Rails 给我们提供了一个后门:

# https://github.com/rails/rails/blob/f916aa247bddba0c58c50822886bc29e8556df76/activerecord/lib/active_record/core.rb#L309-L322
user = User.allocate
user.init_with("attributes" => attributes, "new_record" => false)

我们知道,一个对象由类定义和属性组成,已知类定义之后,我们只要把属性填充给实例对象就完成了初始化过程。其实,这也绕过了使用new方法来构造对象所需的内部复杂工序。

instancate

除此之外,如果你的 attributes 数据是从DB获取来的,你可以使用更高级的instancate方法,它对来源数据进行一次类型转换,然后再调用上述的 allocate + init_withfind_by_sqlSTI就是直接依赖 instancate 来初始化 AR 对象。

# https://github.com/rails/rails/blob/298ec6de55e3adb7d012ba6a9db8b4dd5fd95779/activerecord/lib/active_record/persistence.rb#L56-L70

def instantiate(attributes, column_types = {})
  klass = discriminate_class_for_record(attributes)
  attributes = klass.attributes_builder.build_from_database(attributes, column_types)
  klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end

initialize_dup

如果我们已经有了一个存在的 AR 记录,想再初始化一个的话,使用clonedup也是可行的。关于clonedup区别,我觉得下面这段代码描述是最清晰的:

class Object
  def clone
    clone = self.class.allocate

    clone.copy_instance_variables(self)
    clone.copy_singleton_class(self)

    clone.initialize_clone(self)
    clone.freeze if frozen?

    clone
  end

  def dup
    dup = self.class.allocate
    dup.copy_instance_variables(self)
    dup.initialize_dup(self)
    dup
  end

  def initialize_clone(other)
    initialize_copy(other)
  end

  def initialize_dup(other)
    initialize_copy(other)
  end

  def initialize_copy(other)
    # some internal stuff (don't worry)
  end
end

当然,clonedup的区别这里不是重点,想说一下 initialize_dup 钩子。 initialize_dup 是 AR 内置的,我们在调用 user.dup 时,底层调用的其实是它。注意,user.dup 放回对象把 id 重置为空了,就是说这是一个 shallow copy.

# https://github.com/rails/rails/blob/f916aa247bddba0c58c50822886bc29e8556df76/activerecord/lib/active_record/core.rb#L325-L364

def initialize_dup(other) # :nodoc:
  @attributes = @attributes.dup
  @attributes.reset(self.class.primary_key)

  _run_initialize_callbacks

  @aggregation_cache = {}
  @association_cache = {}

  @new_record  = true
  @destroyed   = false

  super
end

Question

那么问题来了:

  • 为什么 AR 要自己实现一个 initialize_dup,直接 dup 不行么?
  • 我们在缓存的时候是直接把 AR 对象塞进去(即:Rails.cache.write user.id, user),还是自己序列化反序列化?

好吧,其实我是被 Rails 的 serialize attributes 搞疯了...

是直接塞进去的,在 Rails.cache 的 read/write 方法内部已经做过序列化和反序列化了.

user = User.allocate
user.init_with()

好有 Objective-C 的感觉

#1 楼 @quakewang 是的。

但有几个问题,Rails3 之后 AR 对象内部其实非常复杂了,除了最核心的 attributes 属性之外,还有一些内部状态,比如关联缓存也被储存到了实例里,导致直接塞进去之后再读回来 AR 对象不是fresh的(Rails3 时是这样,不知道 Rails4 是否有改动)。如果是片段缓存这种只读场景无所谓,如果可以对从缓存读出的对象再修改,就会带来同步问题。我觉得这也是 initialize_dup 和 encode_with 接口存在的意义。保证 dump 出来的对象是fresh的。

我觉得 Rails 推荐的用法是 用 encode_with dump 出 attributes,用 allocate + init_with 初始化对象,序列化只需要存储的是 attributes,这样对象体积也减少了很多。

# AR内部状态
https://github.com/rails/rails/blob/f916aa247bddba0c58c50822886bc29e8556df76/activerecord/lib/active_record/core.rb#L550-L562
    def init_internals
      @aggregation_cache        = {}
      @association_cache        = {}
      @readonly                 = false
      @destroyed                = false
      @marked_for_destruction   = false
      @destroyed_by_association = nil
      @new_record               = true
      @txn                      = nil
      @_start_transaction_state = {}
      @transaction_state        = nil
      @reflects_state           = [false]
    end

然后 encode_with 这个接口对于 serialize attributes 居然是不 work 的 ... 本质是 attributes_before_type_cast 这个方法返回值不固定: https://github.com/rails/rails/issues/18210

# encode_with imp
  def encode_with(coder)
    # FIXME: Remove this when we better serialize attributes
    coder['raw_attributes'] = attributes_before_type_cast
    coder['attributes'] = @attributes
    coder['new_record'] = new_record?
  end
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册