diff --git a/lib/mongoid/criteria.rb b/lib/mongoid/criteria.rb index 43b8339d8b..f607df5933 100644 --- a/lib/mongoid/criteria.rb +++ b/lib/mongoid/criteria.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require "mongoid/criteria/findable" +require "mongoid/criteria/includable" require "mongoid/criteria/inspectable" require "mongoid/criteria/marshalable" require "mongoid/criteria/modifiable" @@ -19,6 +20,7 @@ class Criteria include Origin::Queryable include Findable include Inspectable + include Includable include Marshalable include Modifiable include Scopable @@ -194,62 +196,6 @@ def initialize(klass) klass ? super(klass.aliased_fields, klass.fields) : super({}, {}) end - # Eager loads all the provided relations. Will load all the documents - # into the identity map whose ids match based on the extra query for the - # ids. - # - # @note This will work for embedded relations that reference another - # collection via belongs_to as well. - # - # @note Eager loading brings all the documents into memory, so there is a - # sweet spot on the performance gains. Internal benchmarks show that - # eager loading becomes slower around 100k documents, but this will - # naturally depend on the specific application. - # - # @example Eager load the provided relations. - # Person.includes(:posts, :game) - # - # @param [ Array ] relations The names of the relations to eager - # load. - # - # @return [ Criteria ] The cloned criteria. - # - # @since 2.2.0 - def includes(*relations) - relations.flatten.each do |name| - metadata = klass.reflect_on_association(name) - raise Errors::InvalidIncludes.new(klass, relations) unless metadata - inclusions.push(metadata) unless inclusions.include?(metadata) - end - clone - end - - # Get a list of criteria that are to be executed for eager loading. - # - # @example Get the eager loading inclusions. - # Person.includes(:game).inclusions - # - # @return [ Array ] The inclusions. - # - # @since 2.2.0 - def inclusions - @inclusions ||= [] - end - - # Set the inclusions for the criteria. - # - # @example Set the inclusions. - # criteria.inclusions = [ meta ] - # - # @param [ Array ] The inclusions. - # - # @return [ Array ] The new inclusions. - # - # @since 3.0.0 - def inclusions=(value) - @inclusions = value - end - # Merges another object with this +Criteria+ and returns a new criteria. # The other object may be a +Criteria+ or a +Hash+. This is used to # combine multiple scopes together, where a chained scope situation diff --git a/lib/mongoid/criteria/includable.rb b/lib/mongoid/criteria/includable.rb new file mode 100644 index 0000000000..08edbfdbb1 --- /dev/null +++ b/lib/mongoid/criteria/includable.rb @@ -0,0 +1,142 @@ +# encoding: utf-8 +module Mongoid + class Criteria + + # Module providing functionality for parsing (nested) inclusion definitions. + module Includable + + # Eager loads all the provided relations. Will load all the documents + # into the identity map whose ids match based on the extra query for the + # ids. + # + # @note This will work for embedded relations that reference another + # collection via belongs_to as well. + # + # @note Eager loading brings all the documents into memory, so there is a + # sweet spot on the performance gains. Internal benchmarks show that + # eager loading becomes slower around 100k documents, but this will + # naturally depend on the specific application. + # + # @example Eager load the provided relations. + # Person.includes(:posts, :game) + # + # @param [ Array, Array ] relations The names of the relations to eager + # load. + # + # @return [ Criteria ] The cloned criteria. + # + # @since 2.2.0 + def includes(*relations) + relations.flatten.each do |relation| + if relation.is_a?(Hash) + extract_nested_inclusion(klass, relation) + else + add_inclusion(klass, relation) + end + end + clone + end + + # Get a list of criteria that are to be executed for eager loading. + # + # @example Get the eager loading inclusions. + # Person.includes(:game).inclusions + # + # @return [ Array ] The inclusions. + # + # @since 2.2.0 + def inclusions + @inclusions ||= [] + end + + # Set the inclusions for the criteria. + # + # @example Set the inclusions. + # criteria.inclusions = [ meta ] + # + # @param [ Array ] The inclusions. + # + # @return [ Array ] The new inclusions. + # + # @since 3.0.0 + def inclusions=(value) + @inclusions = value + end + + private + + # Add an inclusion definition to the list of inclusions for the criteria. + # + # @example Add an inclusion. + # criteria.add_inclusion(Person, :posts) + # + # @param [ Class, String, Symbol ] _klass The class or string/symbol of the class name. + # @param [ Symbol ] relation The relation. + # + # @raise [ Errors::InvalidIncludes ] If no relation is found. + # + # @since 5.1.0 + def add_inclusion(_klass, relation) + metadata = get_inclusion_metadata(_klass, relation) + raise Errors::InvalidIncludes.new(_klass, [ relation ]) unless metadata + inclusions.push(metadata) unless inclusions.include?(metadata) + end + + # Extract inclusion definitions from a list. + # + # @example Extract the inclusions from a list. + # criteria.extract_relations_list(:posts, [{ :alerts => :items }]) + # + # @param [ Symbol ] association The name of the association. + # @param [ Array ] relations A list of associations. + # + # @since 5.1.0 + def extract_relations_list(association, relations) + relations.each do |relation| + if relation.is_a?(Hash) + extract_nested_inclusion(association, relation) + else + add_inclusion(association, relation) + end + end + end + + # Extract nested inclusion. + # + # @example Extract the inclusions from a nested definition. + # criteria.extract_nested_inclusion(User, { :posts => [:alerts] }) + # + # @param [ Class, Symbol ] _klass The class for which the inclusion should be added. + # @param [ Hash ] relation The nested inclusion. + # + # @since 5.1.0 + def extract_nested_inclusion(_klass, relation) + relation.each do |association, _inclusion| + add_inclusion(_klass, association) + if _inclusion.is_a?(Array) + extract_relations_list(association, _inclusion) + else + add_inclusion(association, _inclusion) + end + end + end + + # Get the metadata for an inclusion. + # + # @example Get the metadata for an inclusion definition. + # criteria.get_inclusion_metadata(User, :posts) + # + # @param [ Class, Symbol, String ] _klass The class for determining the association metadata + # @param [ Symbol ] association The name of the association. + # + # @since 5.1.0 + def get_inclusion_metadata(_klass, association) + if _klass.is_a?(Class) + _klass.reflect_on_association(association) + else + _klass.to_s.classify.constantize.reflect_on_association(association) + end + end + end + end +end diff --git a/lib/mongoid/relations/eager.rb b/lib/mongoid/relations/eager.rb index 779a2fb01e..a7952b2b58 100644 --- a/lib/mongoid/relations/eager.rb +++ b/lib/mongoid/relations/eager.rb @@ -34,11 +34,18 @@ def eager_load(docs) end def preload(relations, docs) - - relations.group_by do |metadata| - metadata.relation - end.each do |relation, associations| - relation.eager_load_klass.new(associations, docs).run + grouped_relations = relations.group_by do |metadata| + metadata.inverse_class_name + end + grouped_relations.keys.each do |_klass| + grouped_relations[_klass] = grouped_relations[_klass].group_by do |metadata| + metadata.relation + end + end + grouped_relations.each do |_klass, associations| + docs = associations.collect do |_relation, association| + _relation.eager_load_klass.new(association, docs).run + end.flatten end end end diff --git a/lib/mongoid/relations/eager/base.rb b/lib/mongoid/relations/eager/base.rb index 8d9da20003..0a19601eff 100644 --- a/lib/mongoid/relations/eager/base.rb +++ b/lib/mongoid/relations/eager/base.rb @@ -45,10 +45,12 @@ def shift_metadata # # @since 4.0.0 def run + @loaded = [] while shift_metadata preload + @loaded << @docs.collect { |d| d.send(@metadata.name) } end - @docs + @loaded.flatten end # Preload the current relation. diff --git a/spec/app/models/alert.rb b/spec/app/models/alert.rb index 48ed2eb402..76976c615a 100644 --- a/spec/app/models/alert.rb +++ b/spec/app/models/alert.rb @@ -2,4 +2,6 @@ class Alert include Mongoid::Document field :message, type: String belongs_to :account + has_many :items + belongs_to :post end diff --git a/spec/app/models/post.rb b/spec/app/models/post.rb index 4a8b41cb06..13bf317f2f 100644 --- a/spec/app/models/post.rb +++ b/spec/app/models/post.rb @@ -14,6 +14,7 @@ class Post has_and_belongs_to_many :tags, before_add: :before_add_tag, after_add: :after_add_tag, before_remove: :before_remove_tag, after_remove: :after_remove_tag has_many :videos, validate: false has_many :roles, validate: false + has_many :alerts belongs_to :posteable, polymorphic: true accepts_nested_attributes_for :posteable, autosave: true diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index 19fc523dde..ccfc41c97a 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -1147,12 +1147,72 @@ end end - context "when providing a hash" do + context "when providing a list of associations" do - it "raises an error" do - expect { - Person.includes(preferences: :members) - }.to raise_error(Mongoid::Errors::InvalidIncludes) + let!(:user) do + User.create(posts: [ post1 ], descriptions: [ description1 ]) + end + + let!(:post1) do + Post.create + end + + let!(:description1) do + Description.create(details: 1) + end + + let(:result) do + User.includes(:posts, :descriptions).first + end + + it "executes the query" do + expect(result).to eq(user) + end + + it "includes the related objects" do + expect(result.posts).to eq([ post1 ]) + expect(result.descriptions).to eq([ description1 ]) + end + end + + context "when providing a nested association" do + + let!(:user) do + User.create + end + + before do + p = Post.create(alerts: [ Alert.create ]) + user.posts = [ p ] + user.save + end + + let(:result) do + User.includes(:posts => [:alerts]).first + end + + it "executes the query" do + expect(result).to eq(user) + end + + it "includes the related objects" do + expect(result.posts.size).to eq(1) + expect(result.posts.first.alerts.size).to eq(1) + end + end + + context "when providing a deeply nested association" do + + let!(:user) do + User.create + end + + let(:results) do + User.includes(:posts => [{ :alerts => :items }]).to_a + end + + it "executes the query" do + expect(results.first).to eq(user) end end