Skip to content

Commit

Permalink
Refactoring:
Browse files Browse the repository at this point in the history
- Criteria now lazy loads results unless a one or last call.
- Criteria is now enumerable
- Results are memoized to avoid multiple db calls for a single criteria.
- Finders now integrate with new Criteria API
  • Loading branch information
durran committed Nov 28, 2009
1 parent ede29c3 commit 2ed5a2e
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 208 deletions.
2 changes: 1 addition & 1 deletion lib/mongoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def message
end
end

# Raised when invalid options are passed into an association.
# Raised when invalid options are passed into a constructor.
class InvalidOptionsError < RuntimeError; end

# Connect to the database name supplied. This should be run
Expand Down
149 changes: 100 additions & 49 deletions lib/mongoid/criteria.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,29 @@ module Mongoid #:nodoc:
#
# <tt>criteria.execute</tt>
class Criteria
attr_accessor :klass
attr_reader :selector, :options, :type
include Enumerable

attr_reader :klass, :options, :selector

# Returns true if the supplied +Enumerable+ or +Criteria+ is equal to the results
# of this +Criteria+ or the criteria itself.
#
# This will force a database load when called if an enumerable is passed.
#
# Options:
#
# other: The other +Enumerable+ or +Criteria+ to compare to.
def ==(other)
case other
when Criteria
self.selector == other.selector && self.options == other.options
when Enumerable
execute
return (@collection == other)
else
return false
end
end

AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }"
# Aggregate the criteria. This will take the internally built selector and options
Expand Down Expand Up @@ -54,21 +75,32 @@ def all(selections = {})

# Get the count of matching documents in the database for the +Criteria+.
#
# Options:
#
# klass: Optional class that the collection will be retrieved from.
#
# Example:
#
# <tt>criteria.count</tt>
#
# Returns: <tt>Integer</tt>
def count(klass = nil)
def count
return @count if @count
@klass = klass if klass
return @klass.collection.find(@selector, @options.dup).count
end

# Iterate over each +Document+ in the results. This can take an optional
# block to pass to each argument in the results.
#
# Example:
#
# <tt>criteria.each { |doc| p doc }</tt>
def each
@collection ||= execute
if block_given?
@collection.each { |doc| yield doc }
else
@collection.each
end
end

# Adds a criterion to the +Criteria+ that specifies values that are not allowed
# to match any document in the database. The MongoDB conditional operator that
# will be used is "$ne".
Expand All @@ -89,32 +121,6 @@ def excludes(exclusions = {})
exclusions.each { |key, value| @selector[key] = { "$ne" => value } }; self
end

# Execute the criteria. This will take the internally built selector and options
# and pass them on to the Ruby driver's +find()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned new documents of the type of class provided will be instantiated.
#
# If this is a +Criteria+ to only find the first object, this will return a
# single object of the type of class provided.
#
# If this is a +Criteria+ to find multiple results, will return an +Array+ of
# objects of the type of class provided.
def execute(klass = nil)
@klass = klass if klass
if type == :all
attributes = @klass.collection.find(@selector, @options.dup)
if attributes
@count = attributes.count
return attributes.collect { |doc| @klass.instantiate(doc) }
else
return []
end
else
attributes = @klass.collection.find_one(@selector, @options.dup)
return attributes ? @klass.instantiate(attributes) : nil
end
end

# Adds a criterion to the +Criteria+ that specifies additional options
# to be passed to the Ruby driver, in the exact format for the driver.
#
Expand All @@ -133,6 +139,16 @@ def extras(extras)
self
end

# Return the first result for the +Criteria+.
#
# Example:
#
# <tt>Criteria.select(:name).where(:name = "Chrissy").one</tt>
def one
attributes = @klass.collection.find_one(@selector, @options.dup)
attributes ? @klass.instantiate(attributes) : nil
end

GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
# Groups the criteria. This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
Expand Down Expand Up @@ -196,8 +212,24 @@ def id(object_id)
#
# type: One of :all, :first:, or :last
# klass: The class to execute on.
def initialize(type, klass = nil)
@selector, @options, @type, @klass = {}, {}, type, klass
def initialize(klass)
@selector, @options, @klass = {}, {}, klass
end

# Return the last result for the +Criteria+. Essentially does a find_one on
# the collection with the sorting reversed. If no sorting parameters have
# been provided it will default to ids.
#
# Example:
#
# <tt>Criteria.select(:name).where(:name = "Chrissy").last</tt>
def last
opts = @options.dup
sorting = opts[:sort]
sorting = [[:_id, :asc]] unless sorting
opts[:sort] = sorting.collect { |option| [ option[0], option[1].invert ] }
attributes = @klass.collection.find_one(@selector, opts)
attributes ? @klass.instantiate(attributes) : nil
end

# Adds a criterion to the +Criteria+ that specifies the maximum number of
Expand Down Expand Up @@ -231,7 +263,7 @@ def limit(value = 20)
def merge(other)
case other
when Hash
merge(self.class.translate(:all, other))
merge(self.class.translate(@klass, other))
else
@selector.update(other.selector)
@options.update(other.options)
Expand Down Expand Up @@ -293,17 +325,13 @@ def page

# Executes the +Criteria+ and paginates the results.
#
# Options:
#
# klass: Optional class name to execute the criteria on.
#
# Example:
#
# <tt>criteria.paginate(Person)</tt>
def paginate(klass = nil)
results = execute(klass)
# <tt>criteria.paginate</tt>
def paginate
@collection ||= execute
WillPaginate::Collection.create(page, per_page, count) do |pager|
pager.replace(results)
pager.replace(@collection)
end
end

Expand Down Expand Up @@ -361,16 +389,16 @@ def skip(value = 0)
#
# Example:
#
# <tt>Criteria.translate("4ab2bc4b8ad548971900005c")</tt>
# <tt>Criteria.translate(Person, "4ab2bc4b8ad548971900005c")</tt>
#
# <tt>Criteria.translate(:all, :conditions => { :field => "value"}, :limit => 20)</tt>
# <tt>Criteria.translate(Person, :conditions => { :field => "value"}, :limit => 20)</tt>
#
# Returns a new +Criteria+ object.
def self.translate(*args)
type = args[0] || :all
klass = args[0]
params = args[1] || {}
return new(:first).id(args[0]) unless type.is_a?(Symbol)
return new(type).where(params.delete(:conditions) || {}).extras(params)
return new(klass).id(params).one if params.is_a?(String)
return new(klass).where(params.delete(:conditions) || {}).extras(params)
end

# Adds a criterion to the +Criteria+ that specifies values that must
Expand All @@ -393,6 +421,29 @@ def where(selector = {})
end

protected
# Execute the criteria. This will take the internally built selector and options
# and pass them on to the Ruby driver's +find()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned new documents of the type of class provided will be instantiated.
#
# If this is a +Criteria+ to only find the first object, this will return a
# single object of the type of class provided.
#
# If this is a +Criteria+ to find multiple results, will return an +Array+ of
# objects of the type of class provided.
def execute
attributes = @klass.collection.find(@selector, @options.dup)
if attributes
@count = attributes.count
@collection = attributes.collect { |doc| @klass.instantiate(doc) }
else
@collection = []
end
end

# Filters the unused options out of the options +Hash+. Currently this
# takes into account the "page" and "per_page" options that would be passed
# in if using will_paginate.
def filter_options
page_num = @options.delete(:page)
per_page_num = @options.delete(:per_page)
Expand Down
24 changes: 15 additions & 9 deletions lib/mongoid/finders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ module Finders #:nodoc:
#
# <tt>Person.all(:conditions => { :attribute => "value" })</tt>
def all(*args)
find(:all, *args)
find(*args)
end

# Returns a count of matching records in the database based on the
# provided arguments.
#
# <tt>Person.count(:first, :conditions => { :attribute => "value" })</tt>
def count(*args)
Criteria.translate(*args).count(self)
Criteria.translate(self, *args).count
end

# Find a +Document+ in several different ways.
Expand All @@ -33,7 +33,14 @@ def count(*args)
#
# <tt>Person.find(Mongo::ObjectID.new.to_s)</tt>
def find(*args)
Criteria.translate(*args).execute(self)
type = args.delete_at(0) if args[0].is_a?(Symbol)
criteria = Criteria.translate(self, *args)
case type
when :first then return criteria.one
when :last then return criteria.last
else
return criteria
end
end

# Find the first +Document+ given the conditions.
Expand All @@ -44,7 +51,7 @@ def find(*args)
#
# <tt>Person.first(:conditions => { :attribute => "value" })</tt>
def first(*args)
find(:first, *args)
find(*args).one
end

# Find the last +Document+ given the conditions.
Expand All @@ -55,8 +62,7 @@ def first(*args)
#
# <tt>Person.last(:conditions => { :attribute => "value" })</tt>
def last(*args)
return find(:last, :conditions => {}, :sort => [[:_id, :desc]]) if args.empty?
return find(:last, *args) unless args.empty?
find(*args).last
end

# Will execute a +Criteria+ based on the +DynamicFinder+ that gets
Expand All @@ -73,7 +79,7 @@ def last(*args)
def method_missing(name, *args)
dyna = DynamicFinder.new(name, *args)
finder, conditions = dyna.finder, dyna.conditions
results = Criteria.translate(finder, :conditions => conditions).execute(self)
results = find(finder, :conditions => conditions)
results ? results : dyna.create(self)
end

Expand All @@ -91,7 +97,7 @@ def method_missing(name, *args)
#
# Returns paginated array of docs.
def paginate(params = {})
Criteria.translate(:all, params).paginate(self)
Criteria.translate(self, params).paginate
end

# Entry point for creating a new criteria from a Document. This will
Expand All @@ -108,7 +114,7 @@ def paginate(params = {})
#
# Returns: <tt>Criteria</tt>
def select(*args)
Criteria.new(:all, self).select(*args)
Criteria.new(self).select(*args)
end

end
Expand Down
5 changes: 2 additions & 3 deletions spec/integration/mongoid/document_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@

it "returns an array of documents based on the selector provided" do
documents = Person.find(:all, :conditions => { :title => "Test"})
documents[0].title.should == "Test"
documents.first.title.should == "Test"
end

end
Expand Down Expand Up @@ -181,8 +181,7 @@
end

it "returns a proper count" do
@criteria = Mongoid::Criteria.translate(:all, { :per_page => 20, :page => 1 })
@criteria.execute(Person)
@criteria = Mongoid::Criteria.translate(Person, { :per_page => 20, :page => 1 })
@criteria.count.should == 30
end

Expand Down
Loading

0 comments on commit 2ed5a2e

Please sign in to comment.