Rails 源代码 100 天(8)

根据我们前面的探索,我们已经知道 has_many 方法本身是一个类方法,定义在 ActiveRecord::Associations 这个类中,从方法的定义可以看出:

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

这个方法的第一个动作是去调用 ActiveRecord::Associations::Builder::HasMany 的 build 方法。根据我们之前的探索, build 是被定义在 ActiveRecord::Associations::Builder::Association 这个类中的类方法。又因为如下继承关系:

1
2
class ActiveRecord::Associations::Builder::HasMany < ActiveRecord::Associations::Builder::CollectionAssociation; end
class ActiveRecord::Associations::Builder::CollectionAssociation < ActiveRecord::Associations::Builder::Association; end

ActiveRecord::Associations 这个 module 下,可以直接用 Builder::HasMany 调用 build 方法,而 build 方法的返回值是一个 ActiveRecord::Reflection::HasManyReflection 对象。

我们接下来的目标就是探索这个 reflection 对象在被 new 出来之后,在 build 方法中经过了哪些处理。

先来回顾一下 build 方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

我们在前面的探索中已经知道,这个方法中的局部变量 reflection 已经是一个 ActiveRecord::Reflection::HasManyReflection 对象。接下来这个对象会被传进 define_accessors 等方法。

我们今天的小目标就是理清这个 define_accessors 方法对 reflection 对象做了什么。

根据 Rails 源代码中的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
# activerecord-5.1.4/lib/active_record/associations/builder/association.rb
# Defines the setter and getter methods for the association
# class Post < ActiveRecord::Base
# has_many :comments
# end
#
# Post.first.comments and Post.first.comments= methods are defined by this method...
def self.define_accessors(model, reflection)
mixin = model.generated_association_methods
name = reflection.name
define_readers(mixin, name)
define_writers(mixin, name)
end

这个方法会获得 getter 和 setter 的方法,比如用我们之前提到的例子就是 Product.first.usersProduct.first.users =。具体是怎么实现的,我们来仔细看一下代码。

首先来看,在这个方法的第一行,model 这个参数,也就是调用has_many方法的那个类,会去调用generated_association_methods方法。这个方法是这样的:

1
2
3
4
5
6
7
8
9
10
# activerecord-5.1.4/lib/active_record/core.rb
def generated_association_methods
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
private_constant :GeneratedAssociationMethods
include mod

mod
end
end

假设是我们的Product这个 model 去调用这个方法,这个方法执行时的隐含变量 self 就是 Product 这个类,所以局部变量 mod 会是一个名为Product::GeneratedAssociationMethods的 module。并且这个 module 会被 include,里面的方法会成为Product的实例方法。而对于

1
2
3
@generated_association_methods ||= begin
#...
end

这样的结构,根据经验,大多是为了防止当#generated_association_methods这个方法被多次调用时,里面的实例变量不至于被覆盖。而且,这个方法中的实例变量名和方法名是一样的,符合 ruby 中关于定义 memoization 方法的惯例

什么是 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#<ActiveRecord::Reflection::HasManyReflection:0x007f87472e3698>
@name=:users,
@scope=nil,
@options={},
@active_record=Product(id: integer, name: string, price: decimal, detail: string, created_at: datetime, updated_at: datetime),
@klass=User(id: integer, product_id: integer, name: string, created_at: datetime, updated_at: datetime),
@plural_name="users",
@automatic_inverse_of=:product,
@type=nil,
@foreign_type="users_type",
@constructable=true,
@association_scope_cache={},
@scope_lock=#<Thread::Mutex:0x007f87472e2e78>,
@class_name="User",
@inverse_of=#<ActiveRecord::Reflection::BelongsToReflection:0x007f8748452820>,
@name=:product,
@scope=nil,
@options={},
@active_record=User(id: integer, product_id: integer, name: string, created_at: datetime, updated_at: datetime),
@klass=nil,
@plural_name="products",
@automatic_inverse_of=nil,
@type=nil, @foreign_type="product_type", @constructable=true, @association_scope_cache={},
@scope_lock=#<Thread::Mutex:0x007f87484505e8>
@inverse_which_updates_counter_cache=nil,
@foreign_key="product_id",
@active_record_primary_key="id"

对于这么多的实例变量,我们目前用到的只有@name,剩下的我们以后会用到。

接下来,在define_accessors方法中,rails 又分别调用了define_readersdefine_writers方法,这两个方法是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# activerecord-5.1.4/lib/active_record/associations/builder/association.rb
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

其中参数 mixin 就是我们刚刚新建的 module —— Product::GeneratedAssociationMethods。现在,利用 classeval 方法向这个 module 中动态插入两个方法。根据 name 的值是 :users,插入的来个方法就成了 usersusers=,这是使用非常常用的来个方法。在插入这两个方法的时候,rails 还直到了代码在文件中的位置: `mixin.classeval <<-CODE, FILE, __LINE + 1,这表示代码动态插入的位置在当前文件(activerecord-5.1.4/lib/active_record/associations/builder/association.rb`)的此行 + 1。我们可以验证一下:

1
2
Product.first.method(:users).source_location
# => [ "XXX/activerecord-5.1.4/lib/active_record/associations/builder/association.rb", 110 ]

到这里,我们已经探索完了 define_accessors 方法在构建 reflection 对象时的作用。总结一些就是:

  1. 为调用 has_many 的 model 生成 model::GeneratedAssociationMethods 这个 module
  2. 在这个 module 中动态插入两个 associations 的方法,这样所有 model 的实例对象,就都可以访问 associations 方法了。

不过,我们还有一些问题没有探索,就是在动态生成的 associations 方法,调用了 association 这个方法,这个方法是做什么的。我们将在接下来探索。