MongoDB 让 Mongoid 4.0.0 支持 belongs_to eager load

michael_roshen · 2014年11月05日 · 最后由 michael_roshen 回复于 2014年12月25日 · 9712 次阅读
本帖已被管理员设置为精华贴

mongoid4.0.0 去掉了 IdentityMap

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

模型,假设一张脸有多个左眼和多个右眼吧,懒得改了^^

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

faces = Face.all.to_a
faces.first.left_eyes

MOPED: 127.0.0.1:27017 QUERY runtime: 0.6560ms

[ #, # ]

has_many: eager_load , 使用 .includes 方法

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

[ #, # ]

mongoid 4.0.0 支持 has_many 的 eager_load,但是不支持 belongs_to

eg:

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 参数的时候,会抛出异常

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

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'])

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

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

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

综上,处理 belongs_to eager_load 的做法:

  1. 重写 preload 方法,去掉 raise lib/mongoid/eager_load.rb

    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

    require "mongoid/eager_load"
    
  3. 在 belongs_to 一端,声明 class_name

    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 => #

2.1.1 :002 > eyes = Eye.includes(:eyeable).to_a 2.1.1 :003 > eyes.first.eyeable => #

测试: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 => [ #, # ]

2.1.1 :006 > faces = Face.includes(:left_eyes).to_a 2.1.1 :007 > faces.first.left_eyes => [ #, # ]

博客:http://michael-roshen.iteye.com/blog/2152764 微信:ruby 程序员

完善一下,如果新增一个类 Dog,那么 belongs_to :eyeable, class_name: "Face", polymorphic: true 这里就不能制定 class_name 为 Face 了,因为它有可能是 Dog

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

所以这个类名需要动态的添加,完整代码如下:

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 => [#, #, #, #] 2.1.1 :009 > eyes.first.eyeable => # 2.1.1 :010 > eyes.last.eyeable => # 2.1.1 :011 >

博主 我有个 mongodb 的课程 http://www.hubwiz.com/coursecenter 博主看看 有不对的地方吗?

michael_roshen Rails 4 升级第二弹 提及了此话题。 04月03日 10:57
需要 登录 后方可回复, 如果你还没有账号请 注册新账号