Rails 源代码 100 天(6)

今天的小目标是看看在调用has_many的时候ActiveRecord::Reflection.create(macro, name, scope, options, model)会创造一个怎样的对象。

另外,今天还有一个小调整,就是在引用 rails 的代码是,标注方法定义所在的文件路径。

HasManyReflection的构造函数

我们上次已经说过了在ActiveRecord::Reflection.create方法中,rails 会根据 macro 的内容而 new 出不同的 reflection 对象。对于has_many方法,macro 等于:has_many,因此在 create 方法中会对应到构造一个HasManyReflection这个类的对象:

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

从 create 方法的代码中我们可以看到,传入构造函数的参数有4个,但对于:

1
2
3
4
# app/models/product.rb
class Product < ActiveRecord::Base
has_many :users
end

这个简单的调用来说,scopeoptions都是 nil,而 name 就是:usersar是调用has_many的 ActiveRecord 的类的名称,也就是 model 的名称,在这里就是Product

我们已经了解了传入HasManyReflection.new的参数是什么了,那么接下来就让我们看看这个构造函数的内部是什么样的:

1
2
3
4
5
6
7
# activerecord-5.1.4/lib/active_record/reflection.rb
def initialize(name, scope, options, active_record)
super
@automatic_inverse_of = nil
@type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
# ...
end

其实这个方法是被定义在AssociationReflection这个类中的,而HasManyReflection继承了这个类。而正如这个方法的第一行所示,AssociationReflection也是继承自己他的父类。为了更好的理解这个方法,我们先来看一下其中涉及到的继承关系:

HasManyReflection < AssociationReflection < MacroReflection

MacroReflection的父类也是一个 ActiveRecord 中重要的类,但到MacroReflection为止,我们已经能够获得执行HasManyReflection.new的全部代码。我们先来看看MacroReflection 中的#intialize构造函数,毕竟,那是AssociationReflection#initialize中的第一步(通过上一个代码段中的super方法调用)。

1
2
3
4
5
6
7
8
9
10
# activerecord-5.1.4/lib/active_record/reflection.rb
def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@active_record = active_record
@klass = options[:anonymous_class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end

根据之前的解释,传入这个方法的参数是确定的,分别是:users, nil, nil, Product。通过这个方法,我们将获得6个实例变量,通过对代码的阅读,我们不能知道这些实例变量的值是什么。不过,我们需要另外说明的是,这些实例变量也都是对象的(可以访问的)属性,因为 rails 在MacroReflection这个类中,定义了对应的attr_reader

以上就是MacroReflection#initialize做的事情,接下来我们回到AssociationReflection#initialize中。我们再一次引用它的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# activerecord-5.1.4/lib/active_record/reflection.rb
def initialize(name, scope, options, active_record)
super
@automatic_inverse_of = nil
@type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
@foreign_type = options[:foreign_type] || "#{name}_type"
@constructable = calculate_constructable(macro, options)
@association_scope_cache = {}
@scope_lock = Mutex.new

if options[:class_name] && options[:class_name].class == Class
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Passing a class to the `class_name` is deprecated and will raise
an ArgumentError in Rails 5.2. It eagerloads more classes than
necessary and potentially creates circular dependencies.

Please pass the class name as a string:
`#{macro} :#{name}, class_name: '#{options[:class_name]}'`
MSG
end
end

可以看到,首先这个方法声明了更多的实例变量,这也确实是 ActiveRecord 有关的对象的特点,就是有很多的实例变量。不过,对于has_many :users这种简单的调用来说,@automatic_inverse_of, @type, @foreign_type 都是 nil。 @constructable 在构建HasManyReflection时,会从calculate_constructable方法中得到一个简单的true,但对于处理belongs_to, has_one等方法的调用时,情况会复杂一点,我们以后再说。

另外一个实例变量@association_scope_cache会在以后谈到,而最后一个实例变量@scope_lock = Mutex.new会被赋值一个Mutex对象。什么是Mutex,有什么用,我们将在下一次的文章中来交接。

到这里,我们对于ActiveRecord::Reflection.create(macro, name, scope, options, model)这行代码的作用已经清楚了:在处理has_many的调用时,他会返回一个HasManyReflection对象,这个对象中包含@name, @scope, @options, @klass 等实例变量。而且,可以提前说明的是,这些实例变量将会在接下来被用到。具体在哪里被用到呢?回到create_reflection方法返回的地方:

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

我们可以看到,接下来,这个HasManyReflection对象将会被赋值给reflection这个局部变量,并被传入define_accessors等方法做一些操作。

具体做了那些操作,我们在了解完ruby 中的Mutex后会继续探索。