Skip to content

Commit

Permalink
Merge pull request #4210 from estolfo/MONGOID-4173-nested-loading
Browse files Browse the repository at this point in the history
MONGOID-4173 nested eager loading
  • Loading branch information
estolfo committed Jan 22, 2016
2 parents 8847b32 + a62d525 commit 071b49a
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 67 deletions.
58 changes: 2 additions & 56 deletions lib/mongoid/criteria.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -19,6 +20,7 @@ class Criteria
include Origin::Queryable
include Findable
include Inspectable
include Includable
include Marshalable
include Modifiable
include Scopable
Expand Down Expand Up @@ -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<Symbol> ] 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<Metadata> ] 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<Metadata> ] The inclusions.
#
# @return [ Array<Metadata> ] 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
Expand Down
142 changes: 142 additions & 0 deletions lib/mongoid/criteria/includable.rb
Original file line number Diff line number Diff line change
@@ -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<Symbol>, Array<Hash> ] 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<Metadata> ] 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<Metadata> ] The inclusions.
#
# @return [ Array<Metadata> ] 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
17 changes: 12 additions & 5 deletions lib/mongoid/relations/eager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/mongoid/relations/eager/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions spec/app/models/alert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ class Alert
include Mongoid::Document
field :message, type: String
belongs_to :account
has_many :items
belongs_to :post
end
1 change: 1 addition & 0 deletions spec/app/models/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 65 additions & 5 deletions spec/mongoid/criteria_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 071b49a

Please sign in to comment.