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

adamshen · 发布于 2017年03月02日 · 最后由 wpzero 回复于 2017年03月18日 · 1831 次阅读
20859
本帖已被设为精华帖!

一、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,流程差不多。所以它们的配置方法由父类来定义,然后具体的实现细节由不同的子类来实现。

共收到 10 条回复
96

先赞后学习!

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

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

20859
32ad583255925 回复

这是个常量,它的指向是一个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祖先链的知识

23196

好帖,马后慢慢看!

96
20859adamshen 回复

多谢,涨知识了

96

最近有点高产啊!

20859
32G.O.A.Tzz 回复

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

14939
20859adamshen 回复

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

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