让 mongoid4.0.0 支持 belongs_to eager load


## mongoid4.0.0去掉了IdentityMap
 原来在mongoid3上做的belongs_to eager_load不能再用了
 那我们看看mongoid4.0.0改如何修改

## 模型,假设一张脸有多个左眼和多个右眼吧,懒得改了^^
```ruby
class Face
  include Mongoid::Document
  field :name, type: String
  has_many :left_eyes, class_name: "Eye", as: :eyeable
  has_many :right_eyes, class_name: "Eye", as: :eyeable
end

class Eye
  include Mongoid::Document
  field :name, type: String

  belongs_to :eyeable, polymorphic: true
end
```

## no eager_load
```ruby
faces = Face.all.to_a
faces.first.left_eyes
```

MOPED: 127.0.0.1:27017 QUERY runtime: 0.6560ms

[
#<Eye _id: 545985f06368650e08020000, name: "left eye", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>,
#<Eye _id: 545985f66368650e08030000, name: "left eye2", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>
]

## has_many: eager_load , 使用 .includes方法

```ruby
faces = Face.includes(:left_eyes).to_a
faces.first.left_eyes
```

[
#<Eye _id: 545985f06368650e08020000, name: "left eye", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>,
#<Eye _id: 545985f66368650e08030000, name: "left eye2", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>
]

## mongoid 4.0.0 支持has_many的eager_load,但是不支持 belongs_to
eg:
```ruby
eyes = Eye.includes(:eyeable).to_a
```
抛出异常
Mongoid::Errors::EagerLoad:
Problem:
  Eager loading :eyeable is not supported since it is a
  polymorphic belongs_to relation.

查看原代码,发现当belongs_to 后面接polymorphic参数的时候,会抛出异常
```ruby
module Mongoid
  module Relations
    module Eager
      class BelongsTo < Base
        def preload
           raise Errors::EagerLoad.new(@metadata.name) if @metadata.polymorphic?
           @docs.each do |d|
             set_relation(d, nil)
           end

           each_loaded_document do |doc|
             id = doc.send(key)
             set_on_parent(id, doc)
           end
           ...
        end
      end
    end
  end
end
```
那我们注意掉这句,看一下会发生什么?
NameError: uninitialized constant Eyeable
问题出在这里each_loaded_document, @metadata.klass => Eyeable
这个Eyeable是从哪里来的呢?就是从我们的模型Eye中:belongs_to :eyeable
each_loaded_document这个方法就是load依赖的模型数据了

eyes = Eye.includes(:eyeable).to_a

```ruby
module Mongoid
  module Relations
    module Eager
      class Base
          def each_loaded_document
          @metadata.klass.any_in(key => keys_from_docs).each do |doc|
            yield doc
          end
        end
      end
    end
  end
end
```

找到需要预加载的Face,并通过set_on_parent与eye建立对应关系
@metadata.klass.any_in(key => keys_from_docs) =>
Face.any_in(_id => ['id1','id2'])

```ruby
each_loaded_document do |doc|
   id = doc.send(key)
   set_on_parent(id, doc)
end
```

建立eye对象和face对象的对应关系
#<Eye:0x007f837b55b670>, #<Face:0x007f837b4f2800>
#<Eye:0x007f837b55b5f8>, #<Face:0x007f837b4f2800>

```ruby
def set_on_parent(id, element)
  grouped_docs[id].each do |d|
    set_relation(d, element)
  end
end
```

生成实例变量 @_eyealbe,当调用 eyes.first.eyealbe的时候,则直接返回face对象
而不需要再查询数据库
@_eyeable, #<Face:0x007f837b4f2800>

```ruby
def set_relation(name, relation)
  instance_variable_set("@_#{name}", relation)
end
```   

## 综上,处理belongs_to eager_load的做法:
1. 重写preload方法,去掉raise
lib/mongoid/eager_load.rb
```ruby
module Mongoid
  module Relations
    module Eager
      class BelongsTo < Base
        def preload
           @docs.each do |d|
             set_relation(d, nil)
           end

           each_loaded_document do |doc|
             id = doc.send(key)
             set_on_parent(id, doc)
           end
           ...
        end
      end
    end
  end
end
```
2. 加载initializers/mongoid.rb
```ruby
  require "mongoid/eager_load"
```
3. 在belongs_to一端,声明class_name
```ruby
class Eye
  include Mongoid::Document
  field :name, type: String
  belongs_to :eyeable, class_name: "Face", polymorphic: true
end
```

## 测试:belongs_to
2.1.1 :004 > eyes = Eye.all.to_a
2.1.1 :005 > eyes.first.eyeable
  MOPED: 127.0.0.1:27017 QUERY runtime: 0.6910ms
 => #<Face _id: 545983af6368650e08000000, name: "zhangsan">

2.1.1 :002 > eyes = Eye.includes(:eyeable).to_a
2.1.1 :003 > eyes.first.eyeable
 => #<Face _id: 545983af6368650e08000000, name: "zhangsan">


## 测试:has_many
2.1.1 :006 > faces = Face.includes(:left_eyes).to_a
2.1.1 :009 > faces.first.left_eyes
  MOPED: 127.0.0.1:27017 QUERY runtime: 0.7480ms
 =>
 [
 #<Eye _id: 545985f06368650e08020000, name: "left eye", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>,
 #<Eye _id: 545985f66368650e08030000, name: "left eye2", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>
 ]

2.1.1 :006 > faces = Face.includes(:left_eyes).to_a
2.1.1 :007 > faces.first.left_eyes
 =>
 [
 #<Eye _id: 545985f06368650e08020000, name: "left eye", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>,
 #<Eye _id: 545985f66368650e08030000, name: "left eye2", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>
 ]

完善一下, 如果新增一个类Dog,那么belongs_to :eyeable, class_name: "Face", polymorphic: true
这里就不能制定class_name为Face了,因为它有可能是Dog
```ruby
class Dog
  include Mongoid::Document
  field :name, type: String

  has_many :left_eyes, class_name: "Eye", as: :eyeable
  has_many :right_eyes, class_name: "Eye", as: :eyeable
end
```
所以这个类名需要动态的添加,完整代码如下:

```ruby
module Mongoid
  module Relations
    module Eager
      class Base
        def each_loaded_document_with_polymorphic(&block)
          if @metadata[:polymorphic]
             #查找所有关联的eyeable_type: Dog,Face
            @metadata.inverse_klass.any_in(group_by_key => keys_from_docs).distinct(group_by_key.gsub(/_id$/, '_type')).compact.each do |type|
             #预加载Dog,Face对象
              type.constantize.any_in("_id" => keys_from_docs).each do |doc|
                yield doc
              end 
            end
          else
            each_loaded_document_without_polymorphic(&block)
          end
        end
        alias_method_chain :each_loaded_document, :polymorphic
      end
    end
  end
end
```

测试:
2.1.1 :008 > eyes = Eye.includes(:eyeable).to_a
  MOPED: 127.0.0.1:27017 QUERY   runtime: 0.7560ms
  MOPED: 127.0.0.1:27017 QUERY   runtime: 0.7770ms
  MOPED: 127.0.0.1:27017 QUERY   runtime: 0.4450ms
 => [#<Eye _id: 545985f06368650e08020000, name: "left eye", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>, #<Eye _id: 545985f66368650e08030000, name: "left eye2", eyeable_type: "Face", eyeable_id: BSON::ObjectId('545983af6368650e08000000')>, #<Eye _id: 5459f17e636865148c010000, name: "lisi's eye", eyeable_type: "Face", eyeable_id: BSON::ObjectId('5459f121636865148c000000')>, #<Eye _id: 545b1deb63686517f2010000, name: "dog's left eye", eyeable_type: "Dog", eyeable_id: BSON::ObjectId('545b1dd463686517f2000000')>]
2.1.1 :009 > eyes.first.eyeable
 => #<Face _id: 545983af6368650e08000000, name: "zhangsan">
2.1.1 :010 > eyes.last.eyeable
 => #<Dog _id: 545b1dd463686517f2000000, name: "dog">
2.1.1 :011 >

 

------------------------------------------------------------------------------------------------------------------------------------

另外欢迎关注个人微信订阅号:ruby程序员, 方便大家坐车上下班无聊的时候,共同学习ruby & rails

-------------------------------------------------------------------------------------------------------------------------------------

你可能感兴趣的:(eager_load,belongs_to)