Rails 详解 has_many 方法在 Active Record 中的运行过程

adamshen · 2017年03月02日 · 最后由 wpzero 回复于 2017年03月18日 · 4417 次阅读
本帖已被管理员设置为精华贴

一、Reflection

Reflection 是 ar 用来记录 model 的 associations & aggregations 配置的一个类,不同的方法会创建不同的 Reflection 类型

composed_of 方法会创建一个 AggregateReflection 实例,而 has_many 方法会创建一个 AssociationReflection 实例

# Holds all the methods that are shared between MacroReflection and ThroughReflection.
#                                                                                     
#   AbstractReflection                                                                
#     MacroReflection                                                                 
#       AggregateReflection                                                           
#       AssociationReflection                                                         
#         HasManyReflection                                                           
#         HasOneReflection                                                            
#         BelongsToReflection                                                         
#         HasAndBelongsToManyReflection                                               
#     ThroughReflection                                                               
#       PolymorphicReflection                                                         
#         RuntimeReflection                                                           

Reflection 由统一的一个 builder 来进行实例化,下面的 create 方法创建一个 Reflection,它会把一条 dsl 语句 tokenize 成各种属性集,用来对 association 进行实际的配置

module ActiveRecord
  # = Active Record Reflection
  module Reflection # :nodoc:
    extend ActiveSupport::Concern

    included do
      class_attribute :_reflections, instance_writer: false
      class_attribute :aggregate_reflections, instance_writer: false
      self._reflections = {}
      self.aggregate_reflections = {}
    end

    def self.create(macro, name, scope, options, ar)
      klass = case macro
              when :composed_of
                AggregateReflection
              when :has_many
                HasManyReflection
              when :has_one
                HasOneReflection
              when :belongs_to
                BelongsToReflection
              else
                raise "Unsupported Macro: #{macro}"
              end

      reflection = klass.new(name, scope, options, ar)
      options[:through] ? ThroughReflection.new(reflection) : reflection
    end
    ...
end

比如下面这条语句会生成的 Reflection 如下,然后在创建 association 方法的时候,会调取这个 Reflection 来查询配置

has_many :tracks, -> { order("position") }, dependent: :destroy

 #<ActiveRecord::Reflection::HasManyReflection:0x007fc4ccd7e310
  @active_record=
   CheckCard(id: integer, name: string, title: string, level: integer, submit_times: integer, finish_times: integer, created_at: datetime, updated_at: datetime, user_id: integer, period: string),
  @association_scope_cache={},
  @automatic_inverse_of=nil,
  @constructable=true,
  @foreign_type="tracks_type",
  @klass=nil,
  @name=:tracks,
  @options={:dependent=>:destroy},
  @plural_name="tracks",
  @scope=#<Proc:0x007fc4ccd7e338@/usr/local/lib/ruby/gems/2.4.0/gems/activerecord-5.0.1/lib/active_record/associations/builder/association.rb:55>,
  @scope_lock=#<Thread::Mutex:0x007fc4ccd7e158>,
  @type=nil>]

在 association 设置完毕后,这个 Reflection 会被加入 model 的 Reflections 列表里,可以用 reflect_on_all_associations 方法打印出这个表,有什么用呢?文档上是这么说的

This information, for example, can be used in a form builder that takes an Active Record object and creates input fields for all of the attributes depending on their type and displays the associations to other objects.

Reflection 的实例除了保存配置以外,还存在一个 cache 和一个互斥锁。当进行一次 association 查询以后,相应的 statement 就会存到这个 cache 里,所以重新进行相同的 association 查询时要进行 reload 操作

二、has_many 的运行过程

  module Associations # :nodoc
    module Builder #:nodoc:
      autoload :Association,           'active_record/associations/builder/association'
      autoload :SingularAssociation,   'active_record/associations/builder/singular_association'
      autoload :CollectionAssociation, 'active_record/associations/builder/collection_association'

      autoload :BelongsTo,           'active_record/associations/builder/belongs_to'
      autoload :HasOne,              'active_record/associations/builder/has_one'
      autoload :HasMany,             'active_record/associations/builder/has_many'
      autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
    end

    module ClassMethod
     def has_many(name, scope = nil, options = {}, &extension)
        reflection = Builder::HasMany.build(self, name, scope, options, &extension)
        Reflection.add_reflection self, name, reflection
      end
    end
end

has_many 方法只有两行,它的实现方式全部都被封装到了 ActiveRecord::Associations::Builder::HasMany 类里

Builder::HasMany.build 方法不仅创建了 reflection,还同时对 association 进行了配置

这里类的继承关系是 Builder::HasMany < Builder::CollectionAssociation < Builder::Association

# The hierarchy is defined as follows:
#  Association
#    - SingularAssociation
#      - BelongsToAssociation
#      - HasOneAssociation
#    - CollectionAssociation
#      - HasManyAssociation

Builder::HasMany.build 方法由父类 Builder::Association 类来实现,它创建了一个 Reflection,定义了各种 accessors 方法,以及 callback 和 validations

module ActiveRecord::Associations::Builder # :nodoc:
  class Association #:nodoc:
    def self.build(model, name, scope, options, &block)
      if model.dangerous_attribute_method?(name)
        raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
                             "this will conflict with a method #{name} already defined by Active Record. " \
                             "Please choose a different association name."
      end

      extension = define_extensions model, name, &block
      reflection = create_reflection model, name, scope, options, extension
      define_accessors model, reflection
      define_callbacks model, reflection
      define_validations model, reflection
      reflection
    end

接下来逐步看一下它的运行流程,首先检查一下方法名称是否已经被定义,然后生成一个 extension。define_extensions 方法在 CollectionAssociation 类里

In Model...

class Author < ApplicationRecord
  has_many :books do
    def find_by_book_prefix(book_number)
      find_by(category_id: book_number[0..2])
    end
  end
end

method...
    def self.define_extensions(model, name)
      if block_given?
        extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
        extension = Module.new(&Proc.new)
        model.parent.const_set(extension_module_name, extension)
      end
    end

老实说这个方法,呃,十分有趣

首先假如 has_many 方法有 block,这个 block 会被传递给 Proc.new,然后再用&符号转成 block。也就是说,原来的 block 被通过一个 Proc 的中转交给了新建的 Module

这个新建的 Module 会立即在他的 scope 下运行这个 block。所以上面定义的这个 find_by_book_prefix 方法在新建的这个 Module 上被定义了

最后把新建的这个 Module 绑定在一个常量上,这个常量属于 model 的 parent 类,总之看起来既让人迷惑又觉得很酷

这个新建 Module 有什么用呢?其实和用 lambda 定义的 scope 作用是一样的,如果你同时用了 lambda 定义 scope,那么它们会呈链式调用

def self.wrap_scope(scope, mod)
  if scope
    if scope.arity > 0
      proc { |owner| instance_exec(owner, &scope).extending(mod) }
    else
      proc { instance_exec(&scope).extending(mod) }
    end
  else
    proc { extending(mod) }
  end
end

接下来是创建 Reflection 的方法

def self.create_reflection(model, name, scope, options, extension = nil)
  raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)

  if scope.is_a?(Hash)
    options = scope
    scope   = nil
  end

  validate_options(options)

  scope = build_scope(scope, extension)

  ActiveRecord::Reflection.create(macro, name, scope, options, model)
end

首先检查参数,允许没有 lambda 的情况,options 提前。然后校验 options 的 keys 是否合法。合并 lambda 和 extension,最后创建 reflection

接下来是生成 accessor 方法

def self.define_accessors(model, reflection)
  mixin = model.generated_association_methods
  name = reflection.name
  define_readers(mixin, name)
  define_writers(mixin, name)
end

def generated_association_methods
  @generated_association_methods ||= begin
    mod = const_set(:GeneratedAssociationMethods, Module.new)
    include mod
    mod
  end
end

这里生成的 accessor 方法并不是直接定义在 model 上,而是先新建一个 Module,include 到 model 里,然后再把各种方法定义到这个 Module 上。我猜这样做是为了可以让你自己在 model 定义同名方法,并使用 default_scope 调用原方法

Builder::Association 会定义类似 student.books 以及 student.books=这两个方法,Builder:: CollectionAssociation 扩展定义了_ids 方法

In super...
    def self.define_readers(mixin, name)
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}(*args)
          association(:#{name}).reader(*args)
        end
      CODE
    end

    def self.define_writers(mixin, name)
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}=(value)
          association(:#{name}).writer(value)
        end
      CODE
    end

In child...
    # Defines the setter and getter methods for the collection_singular_ids.
    def self.define_readers(mixin, name)
      super

      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name.to_s.singularize}_ids
          association(:#{name}).ids_reader
        end
      CODE
    end

    def self.define_writers(mixin, name)
      super

      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name.to_s.singularize}_ids=(ids)
          association(:#{name}).ids_writer(ids)
        end
      CODE
    end

再接下来是定义 callbacks,关于 callbacks 链可以见上一篇文章

这里没有做什么特别的事情,只是从 Reflection 中提取 callback 的配置,然后写到相对应的 callbacks 链中

CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]

def self.define_callbacks(model, reflection)
  super
  name    = reflection.name
  options = reflection.options
  CALLBACKS.each { |callback_name|
    define_callback(model, callback_name, name, options)
  }
end

def self.define_callback(model, callback_name, name, options)
  full_callback_name = "#{callback_name}_for_#{name}"

  # TODO : why do i need method_defined? I think its because of the inheritance chain
  model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
  callbacks = Array(options[callback_name.to_sym]).map do |callback|
    case callback
    when Symbol
      ->(method, owner, record) { owner.send(callback, record) }
    when Proc
      ->(method, owner, record) { callback.call(owner, record) }
    else
      ->(method, owner, record) { callback.send(method, owner, record) }
    end
  end
  model.send "#{full_callback_name}=", callbacks
end

最后 define_validations,Builder::HasMany 并没有覆写这个方法,所以这里什么也没干

三、总结

声明 associations 的各个方法 belongs_to、has_many 的运行过程分两步。一是创建 Reflection,二是把 Reflection 当参数丢过去来进行实际配置。

运行过程由 Builder 控制,因为 associations 的 dsl 基本都需要创建读写命令和 callbacks,流程差不多。所以它们的配置方法由父类来定义,然后具体的实现细节由不同的子类来实现。

先赞后学习!

jasl 将本帖设为了精华贴。 03月02日 14:59

我想问下 generated_association_methods 这个方法返回的是什么?我 puts 了一下返回的还是 model 啊,方法是怎么绑到 GeneratedAssociationMethods 这个变量上的

ad583255925 回复

这是个常量,它的指向是一个 module,这个 module 被加入到了 model 的祖先链里了

然后用 class_eval 的方式来绑定方法

ruby 有个特性,就是你 include module 进来的时候,会复制一个 Rclass 结构体,但是它的方法表是指向原 Rclass 结构体的

所以你先 include 一个 module 进来,然后再改动这个 module 的方法,你的执行 include 的这个 class 也可以调用新生成的方法

这就是为什么可以先生成一个空的 module,include 进来以后再 define 属于这个 module 的方法

你看看下面的命令结果,我的 model 是 CheckCard,has_many check_items

如果还不明白,你需要补充 ruby 祖先链的知识

好帖,马后慢慢看!

adamshen 回复

多谢,涨知识了

最近有点高产啊!

G.O.A.Tzz 回复

😅 反正孤独的人无所谓,无日无夜无条件

adamshen 回复

😀 虽然你孤独,但是你孩子多,产量高啊

需要 登录 后方可回复, 如果你还没有账号请 注册新账号