Rails source code 100 days (8)

According to the previous exploration of us, we already know that has_manythe method itself is a class method, defined in ActiveRecord::Associationsthis class, the method can be seen from the definition:

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

The first action of this method is to call the ActiveRecord::Associations::Builder::HasManybuild process. According to our previous exploration, buildit is defined in the ActiveRecord::Associations::Builder::Associationclass methods of this class. And because inheritance as follows:

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

ActiveRecord::AssociationsUnder this Module1, you can directly Builder::HasManycall build method and build the return value is an ActiveRecord::Reflection::HasManyReflectionobject.

Our next goal is to explore this new reflection object after being out, after which treatment in the build process.

First look at the code build process:

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

We already know that in previous exploration, local variables reflection of this approach is already an ActiveRecord::Reflection::HasManyReflectionobject. Next, the object will be transferred into define_accessorsother methods.

Our goal today is to sort out small this define_accessorsmethod on reflectionthe object to do something.

The Rails source code comments:

1
2
3
4
5
6
7
8
9
10
11
12
13

# 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

This method will get getter and setter methods, such as with our previous example is mentioned Product.first.usersand Product.first.users =. Specifically how to achieve, we take a closer look at the code.

First look at the first line of this method, model this parameter, which is calling has_manythe class method call will go generated_association_methodsmethod. This method is this:

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


Our assumption is that Productthis model to call this method, the implicit variables of this method to perform self is Productthe class, so the mod will be a local variable named Product::GeneratedAssociationMethodsmodule of. And this module will be include, a method which will be Productthe example method. For

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

With this configuration, based on experience, to prevent most if #generated_association_methodswhen the method is called multiple times, which will not be covered by the instance variable. Moreover, instance variables and method names in this method is the same, in line with the convention on the definition of ruby memoization method .

What is the big column  Rails source code is 100 days (8) S: //en.wikipedia.org/wiki/Memoization "target =" _ blank "rel =" noopener noreferrer "> memoization method described in Wikipedia, which is a? when the seed part of the calculation result is stored up, next time call the same calculation, you can directly deposit out good results, without re-calculation. this helps performance commission program, can also be used when multiple calls to guarantee results consistency.

Back define_accessorsmethod, we already know, mixin is a name GeneratedAssociationMethodsof the empty module.

Then, we have to remove the name attribute of reflection. However, creating an object of reflection time, after a lot of layers of abstraction, I did not know you still remember what the name attribute in the end yes. Looking from the source code, in HasManyReflectionan ancestor ( MacroReflection), there is a constructor creates the getter method name, the name is something that has_many :usersthis call incoming symbol, therefore, is the 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

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 这个方法,这个方法是做什么的。我们将在接下来探索。

Guess you like

Origin www.cnblogs.com/liuzhongrong/p/12346025.html