MongoDB 让 Mongoid 4.0.0 支持 belongs_to eager load

michael_roshen · 2014年11月05日 · 最后由 michael_roshen 回复于 2014年12月25日 · 5389 次阅读
本帖已被设为精华帖!

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程序员

共收到 3 条回复

完善一下, 如果新增一个类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 博主看看 有不对的地方吗?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册