-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Major - P3
-
None
-
Affects Version/s: 9.1.1
-
Component/s: Associations
-
None
Problem
When I use includes with nested associations, Mongoid ignores the nested part if the parent association's class_name is not exactly the class name. This happens in two common cases:
- class_name: "::Photo" (the name starts with "::")
- class_name: "Item" inside the module Shop. Mongoid finds the class Shop::Item.
Mongoid still eager loads the parent association, but it does not eager load the nested associations. It raises no error. Each nested association is then loaded with one query per document (N+1).
Versions: Mongoid 9.1.1, Ruby driver 2.26.0, Ruby 3.3.11. The same code is on master.
How to reproduce
Run this script. Set MONGO_HOST if MongoDB is not on 127.0.0.1:27017.
Each case has 3 parents. Each parent has 1 child, and each child has 1 author. When nested includes work, Mongoid loads all authors with 1 query.
# frozen_string_literal: true require "mongoid" Mongoid.configure do |config| config.clients.default = { hosts: [ENV.fetch("MONGO_HOST", "127.0.0.1:27017")], database: "mongoid_nested_includes_repro" } end Mongo::Logger.logger.level = Logger::WARN class FindCounter attr_reader :finds def initialize = @finds= Hash.new(0) def started(event) = (finds[event.command["find"]] += 1 if event.command_name == "find") def succeeded(_) = nil def failed(_) = nil end class Author include Mongoid::Document end class Gallery include Mongoid::Document has_many :plain_photos, class_name: "Photo", inverse_of: :gallery has_many :rooted_photos, class_name: "::Photo", inverse_of: :gallery end class Photo include Mongoid::Document belongs_to :gallery belongs_to :author end module Shop class Order include Mongoid::Document has_many :items, class_name: "Item", inverse_of: :order end class Item include Mongoid::Document belongs_to :order, class_name: "Order", inverse_of: :items belongs_to :author, class_name: "::Author" end end Mongoid.purge! 3.times do gallery = Gallery.create! order = Shop::Order.create! Photo.create!(gallery: gallery, author: Author.create!) Shop::Item.create!(order: order, author: Author.create!) end def author_finds(label) counter = FindCounter.new subscriber = Mongoid.default_client.subscribe(Mongo::Monitoring::COMMAND, counter) yield Mongoid.default_client.unsubscribe(Mongo::Monitoring::COMMAND, subscriber) puts format("%-62s authors finds: %d", label, counter.finds["authors"]) end puts "mongoid #{Mongoid::VERSION}, driver #{Mongo::VERSION}, ruby #{RUBY_VERSION}" puts "rooted_photos class_name=#{Gallery.relations['rooted_photos'].class_name.inspect} klass=#{Gallery.relations['rooted_photos'].klass}" puts "items class_name=#{Shop::Order.relations['items'].class_name.inspect} klass=#{Shop::Order.relations['items'].klass}" author_finds('Gallery.includes(plain_photos: :author) [class_name "Photo"]') { Gallery.includes(plain_photos: :author).each { |g| g.plain_photos.each(&:author) } } author_finds('Gallery.includes(rooted_photos: :author) [class_name "::Photo"]') { Gallery.includes(rooted_photos: :author).each { |g| g.rooted_photos.each(&:author) } } author_finds('Shop::Order.includes(items: :author) [class_name "Item"]') { Shop::Order.includes(items: :author).each { |o| o.items.each(&:author) } } Mongoid.default_client.database.drop
Actual result
Gallery.includes(plain_photos: :author) [class_name "Photo"] authors finds: 1 Gallery.includes(rooted_photos: :author) [class_name "::Photo"] authors finds: 3 Shop::Order.includes(items: :author) [class_name "Item"] authors finds: 3
Expected result
authors finds: 1 in all three cases.
Cause
In Mongoid::Association::EagerLoadable#preload:
assoc_map = associations.group_by(&:inverse_class_name)
# ...
queue << assoc.class_name
The keys of assoc_map are real class names, like "Photo" or "Shop::Item". They come from Criteria::Includable#extract_includes_list, which calls extract_includes_list(association.klass, ...) for nested includes.
But the queue gets the raw :class_name option, like "::Photo" or "Item". These values never match a key, so Mongoid skips the nested associations.
Possible fix
Put the real class name in the queue:
queue << (assoc.polymorphic? ? assoc.class_name : assoc.klass.name)
With this change, the script prints authors finds: 1 in all three cases.
This change adds no new class lookups. extract_includes_list already calls association.klass for every association with nested includes, and the eager loader already calls @association.klass for every association it loads.
Related tickets
- MONGOID-4751: the same kind of problem (relation_class_name is compared with class names). The preload queue is not in its list.
MONGOID-4882: the same "::" problem, fixed for embedded associations.MONGOID-4716: added the nested preload queue.