根据我们前面的探索,我们已经知道 has_many
方法本身是一个类方法,定义在 ActiveRecord::Associations
这个类中,从方法的定义可以看出:
1 |
def (name, scope = nil, options = {}, &extension) |
这个方法的第一个动作是去调用 ActiveRecord::Associations::Builder::HasMany
的 build 方法。根据我们之前的探索, build
是被定义在 ActiveRecord::Associations::Builder::Association
这个类中的类方法。又因为如下继承关系:
1 |
class ActiveRecord::Associations::Builder::HasMany < ActiveRecord::Associations::Builder::CollectionAssociation; end |
ActiveRecord::Associations
这个 module 下,可以直接用 Builder::HasMany
调用 build 方法,而 build 方法的返回值是一个 ActiveRecord::Reflection::HasManyReflection
对象。
我们接下来的目标就是探索这个 reflection 对象在被 new 出来之后,在 build 方法中经过了哪些处理。
先来回顾一下 build 方法的代码:
1 |
def self.build(model, name, scope, options, &block) |
我们在前面的探索中已经知道,这个方法中的局部变量 reflection 已经是一个 ActiveRecord::Reflection::HasManyReflection
对象。接下来这个对象会被传进 define_accessors
等方法。
我们今天的小目标就是理清这个 define_accessors
方法对 reflection
对象做了什么。
根据 Rails 源代码中的注释:
1 |
|
这个方法会获得 getter 和 setter 的方法,比如用我们之前提到的例子就是 Product.first.users
和 Product.first.users =
。具体是怎么实现的,我们来仔细看一下代码。
首先来看,在这个方法的第一行,model 这个参数,也就是调用has_many
方法的那个类,会去调用generated_association_methods
方法。这个方法是这样的:
1 |
# activerecord-5.1.4/lib/active_record/core.rb |
假设是我们的Product
这个 model 去调用这个方法,这个方法执行时的隐含变量 self 就是 Product
这个类,所以局部变量 mod 会是一个名为Product::GeneratedAssociationMethods
的 module。并且这个 module 会被 include,里面的方法会成为Product
的实例方法。而对于
1 |
@generated_association_methods ||= begin |
这样的结构,根据经验,大多是为了防止当#generated_association_methods
这个方法被多次调用时,里面的实例变量不至于被覆盖。而且,这个方法中的实例变量名和方法名是一样的,符合 ruby 中关于定义 memoization 方法的惯例。
什么是 大专栏 Rails 源代码 100 天(8)s://en.wikipedia.org/wiki/Memoization" target="_blank" rel="noopener noreferrer">memoization 方法?根据维基百科的描述,这是一种把一部分计算结果存储起来,下次再调用相同的计算时,就可以直接取出存好的结果,而不用重新计算。这有助于提成程序的性能,也可以用于保证多次调用时结果具有一致性。
回到define_accessors
方法中,我们已经知道,mixin 是一个名字叫 GeneratedAssociationMethods
的空 module。
紧接着,我们要取出 reflection 中的 name 属性。但是,创建 reflection 对象的时候,经过了很多层抽象,不知道你还记不记得 name 属性到底是什么。从源代码中找,在HasManyReflection
的一个祖先(MacroReflection
)中,有一个构造方法创建了 name 这个 getter 方法,那事的 name 就是 has_many :users
这个调用中传入的 symbol,所以,name 就是 :users
。
另外,我们也可以从 Product.first.users
得到 User::ActiveRecord_Associations_CollectionProxy
对象了中取出这个 reflection 对象 —— Product.first.users.instance_variable_get(:@association).reflection
,这个对象了中的实例变量如下:
1 |
#<ActiveRecord::Reflection::HasManyReflection:0x007f87472e3698> |
对于这么多的实例变量,我们目前用到的只有@name
,剩下的我们以后会用到。
接下来,在define_accessors
方法中,rails 又分别调用了define_readers
和define_writers
方法,这两个方法是这样的:
1 |
|
其中参数 mixin 就是我们刚刚新建的 module —— Product::GeneratedAssociationMethods
。现在,利用 classeval 方法向这个 module 中动态插入两个方法。根据 name 的值是 :users
,插入的来个方法就成了 users
和 users=
,这是使用非常常用的来个方法。在插入这两个方法的时候,rails 还直到了代码在文件中的位置: `mixin.classeval <<-CODE, FILE, __LINE + 1,这表示代码动态插入的位置在当前文件(
activerecord-5.1.4/lib/active_record/associations/builder/association.rb`)的此行 + 1。我们可以验证一下:
1 |
Product.first.method(:users).source_location |
到这里,我们已经探索完了 define_accessors
方法在构建 reflection 对象时的作用。总结一些就是:
- 为调用 has_many 的 model 生成 model::GeneratedAssociationMethods 这个 module
- 在这个 module 中动态插入两个 associations 的方法,这样所有 model 的实例对象,就都可以访问 associations 方法了。
不过,我们还有一些问题没有探索,就是在动态生成的 associations 方法,调用了 association
这个方法,这个方法是做什么的。我们将在接下来探索。