Skip to content

Commit

Permalink
Scopes and sexy querying now work on many document associations. Had …
Browse files Browse the repository at this point in the history
…to do a lot more work than I was planning on for this.
  • Loading branch information
jnunemaker committed Jun 18, 2010
1 parent 3ab5986 commit 2d8672d
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 117 deletions.
42 changes: 12 additions & 30 deletions lib/mongo_mapper/plugins/associations/many_documents_proxy.rb
Expand Up @@ -4,34 +4,7 @@ module Plugins
module Associations
class ManyDocumentsProxy < Collection
include DynamicQuerying::ClassMethods

def find(*args)
query.find(*args)
end

def find!(*args)
query.find!(*args)
end

def paginate(options)
query.paginate(options)
end

def all(options={})
query(options).all
end

def first(options={})
query(options).first
end

def last(options={})
query(options).last
end

def count(options={})
query(options).count
end
include Querying::PluckyMethods

def replace(docs)
load_target
Expand Down Expand Up @@ -93,8 +66,17 @@ def save_to_collection(options={})
def query(options={})
klass.
query(association.query_options).
update(options).
update(criteria)
update(options).update(criteria)
end

def method_missing(method, *args, &block)
if klass.respond_to?(method)
result = klass.send(method, *args, &block)
result.is_a?(Plucky::Query) ?
query.merge(result) : super
else
super
end
end

def criteria
Expand Down
2 changes: 1 addition & 1 deletion lib/mongo_mapper/plugins/document.rb
Expand Up @@ -18,7 +18,7 @@ def destroyed?
end

def reload
if doc = self.class.query.find_one(:_id => id)
if doc = collection.find_one(:_id => id)
tap do |instance|
instance.class.associations.each_key do |association_name|
send(association_name).reset if respond_to?(association_name)
Expand Down
41 changes: 24 additions & 17 deletions lib/mongo_mapper/plugins/identity_map.rb
Expand Up @@ -30,21 +30,34 @@ def identity_map=(v)
@identity_map = v
end

def find_one(options={})
query = query(options)

if query.simple? && identity_map.key?(query[:_id])
identity_map[query[:_id]]
else
super.tap do |document|
remove_documents_from_map(document) if query.fields?
module IdentityMapQueryMethods
def all(opts={})
query = clone.update(opts)
super.tap do |docs|
model.remove_documents_from_map(docs) if query.fields?
end
end

def find_one(opts={})
query = clone.update(opts)

if query.simple? && model.identity_map[query[:_id]]
model.identity_map[query[:_id]]
else
super.tap do |doc|
model.remove_documents_from_map(doc) if query.fields?
end
end
end
end

def find_many(options)
super.tap do |documents|
remove_documents_from_map(documents) if query(options).fields?
def query(opts={})
super.extend(IdentityMapQueryMethods)
end

def remove_documents_from_map(*documents)
documents.flatten.compact.each do |document|
identity_map.delete(document['_id'])
end
end

Expand Down Expand Up @@ -88,12 +101,6 @@ def without_identity_map(&block)
end

private
def remove_documents_from_map(*documents)
documents.flatten.compact.each do |document|
identity_map.delete(document._id)
end
end

def selecting_fields?(options)
!options[:fields].nil?
end
Expand Down
49 changes: 2 additions & 47 deletions lib/mongo_mapper/plugins/querying.rb
Expand Up @@ -8,59 +8,14 @@ module Querying
module ClassMethods
include PluckyMethods

def find(*args)
options = args.extract_options!
return nil if args.size == 0

if args.first.is_a?(Array) || args.size > 1
find_some(args, options)
else
find_one(options.merge(:_id => args[0]))
end
end

def find!(*args)
options = args.extract_options!
raise DocumentNotFound, "Couldn't find without an ID" if args.size == 0

if args.first.is_a?(Array) || args.size > 1
find_some!(args, options)
else
find_one(options.merge(:_id => args[0])) ||
raise(DocumentNotFound, "Document match #{options.inspect} does not exist
in #{collection.name} collection")
end
end

def find_each(options={})
query(options).find_each.each { |doc| yield load(doc) }
def find_each(opts={})
super(opts).each { |doc| yield load(doc) }
end

def find_by_id(id)
find_one(:_id => id)
end

def first(options={})
find_one(options)
end

# All bets are off an actual order if you provide none.
def last(options={})
find_one(query(options).reverse.to_hash)
end

def all(options={})
find_many(options)
end

def count(options={})
query(options).count
end

def exists?(options={})
!count(options).zero?
end

def first_or_create(args)
first(args) || create(args.reject { |key, value| !key?(key) })
end
Expand Down
25 changes: 6 additions & 19 deletions lib/mongo_mapper/plugins/querying/plucky_methods.rb
@@ -1,27 +1,14 @@
# encoding: UTF-8
require 'forwardable'

module MongoMapper
module Plugins
module Querying
module PluckyMethods
def where(options={})
query.where(options)
end

def fields(*args)
query.fields(*args)
end

def limit(*args)
query.limit(*args)
end

def skip(*args)
query.skip(*args)
end

def sort(*args)
query.sort(*args)
end
extend Forwardable
def_delegators :query, :where, :fields, :limit, :skip, :sort,
:count, :last, :first, :all, :paginate,
:find, :find!, :exists?, :exist?, :find_each
end
end
end
Expand Down
39 changes: 39 additions & 0 deletions test/functional/associations/test_many_documents_proxy.rb
Expand Up @@ -345,6 +345,45 @@ def setup
end
end

context "sexy querying" do
should "work with where" do
@project1.statuses.where(:name => 'New').all.should == [@brand_new]
end

should "work with sort" do
@project1.statuses.sort(:name).all.should == [@complete, @brand_new]
end

should "work with limit" do
@project1.statuses.sort(:name).limit(1).all.should == [@complete]
end

should "work with skip" do
@project1.statuses.sort(:name).skip(1).all.should == [@brand_new]
end

should "work with fields" do
@project1.statuses.fields(:position).all.each do |status|
status.position.should_not be_nil
status.name.should be_nil
end
end

should "work with scopes" do
@project1.statuses.complete.all.should == [@complete]
end

should "work with methods on class that return query" do
@project1.statuses.by_position(1).first.should == @brand_new
end

should "not work with methods on class that do not return query" do
Status.class_eval { def self.foo; 'foo' end }
lambda { @project1.statuses.foo }.
should raise_error(NoMethodError)
end
end

context "all" do
should "work" do
@project1.statuses.all(:order => "position asc").should == [@brand_new, @complete]
Expand Down
6 changes: 3 additions & 3 deletions test/functional/test_identity_map.rb
Expand Up @@ -327,7 +327,7 @@ def expects_one_query

should "return nil for document id not found in collection" do
assert_in_map(@person)
@person_class.find_by_id(1234).should be_nil
@person_class.find_by_id(BSON::ObjectID.new).should be_nil
end
end

Expand All @@ -342,13 +342,13 @@ def expects_one_query
@person_class.first(:_id => @person.id, :select => 'name').should == @person
@person_class.first(:_id => @person.id, 'fields' => ['name']).should == @person
@person_class.last(:_id => @person.id, :select => 'name', :order => 'name').should == @person
@person_class.find(@person.id, :select => 'name').should == @person
@person_class.fields(:name).find(@person.id).should == @person
@person_class.all(:_id => @person.id, :select => 'name').should == [@person]
assert_not_in_map(@person)
end

should "return nil if not found" do
@person_class.find(1234, :select => 'name').should be_nil
@person_class.fields(:name).find(BSON::ObjectID.new).should be_nil
end
end

Expand Down
6 changes: 6 additions & 0 deletions test/models.rb
Expand Up @@ -143,6 +143,12 @@ class Collaborator
class Status
include MongoMapper::Document

scope :complete, where(:name => 'Complete')

def self.by_position(position)
where(:position => position)
end

key :project_id, ObjectId
key :target_id, ObjectId
key :target_type, String
Expand Down

0 comments on commit 2d8672d

Please sign in to comment.