diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 10712047..0742244d 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -86,6 +86,41 @@ achieving this is using array-slicing syntax:: # 5 users, starting from the 10th user found users = User.objects[10:15] +You may also index the query to retrieve a single result. If an item at that +index does not exists, an :class:`IndexError` will be raised. A shortcut for +retrieving the first result and returning :attr:`None` if no result exists is +provided (:meth:`~mongoengine.queryset.QuerySet.first`):: + + >>> # Make sure there are no users + >>> User.drop_collection() + >>> User.objects[0] + IndexError: list index out of range + >>> User.objects.first() == None + True + >>> User(name='Test User').save() + >>> User.objects[0] == User.objects.first() + True + +Retrieving unique results +------------------------- +To retrieve a result that should be unique in the collection, use +:meth:`~mongoengine.queryset.QuerySet.get`. This will raise +:class:`~mongoengine.queryset.DoesNotExist` if no document matches the query, +and :class:`~mongoengine.queryset.MultipleObjectsReturned` if more than one +document matched the query. + +A variation of this method exists, +:meth:`~mongoengine.queryset.Queryset.get_or_create`, that will create a new +document with the query arguments if no documents match the query. An +additional keyword argument, :attr:`defaults` may be provided, which will be +used as default values for the new document, in the case that it should need +to be created:: + + >>> a = User.objects.get_or_create(name='User A', defaults={'age': 30}) + >>> b = User.objects.get_or_create(name='User A', defaults={'age': 40}) + >>> a.name == b.name and a.age == b.age + True + Default Document queries ======================== By default, the objects :attr:`~mongoengine.Document.objects` attribute on a diff --git a/docs/index.rst b/docs/index.rst index 1205e4b9..6e0db8b0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,11 @@ MongoDB. To install it, simply run The source is available on `GitHub `_. +If you are interested in contributing, join the developers' `mailing list +`_. Some of us also like to +hang out at `#mongoengine IRC channel `_. + + .. toctree:: :maxdepth: 2 diff --git a/mongoengine/base.py b/mongoengine/base.py index b4920eec..b658f326 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -352,19 +352,15 @@ class BaseDocument(object): if value is not None: data[field.name] = field.to_mongo(value) # Only add _cls and _types if allow_inheritance is not False - #if not (hasattr(self, '_meta') and - # self._meta.get('allow_inheritance', True) == False): - ah = True - if hasattr(self, '_meta'): - ah = self._meta.get('allow_inheritance', True) - if ah: + if not (hasattr(self, '_meta') and + self._meta.get('allow_inheritance', True) == False): data['_cls'] = self._class_name data['_types'] = self._superclasses.keys() + [self._class_name] return data @classmethod def _from_son(cls, son): - """Create an instance of a Document (subclass) from a PyMongo SOM. + """Create an instance of a Document (subclass) from a PyMongo SON. """ # get the class name from the document, falling back to the given # class if unavailable diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index e71f0598..f2405b82 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -10,6 +10,7 @@ __all__ = ['queryset_manager', 'Q', 'InvalidQueryError', # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 + class DoesNotExist(Exception): pass @@ -299,37 +300,46 @@ class QuerySet(object): return mongo_query - def get_or_create(self, **kwargs): - """Retreive unique object or create, if it doesn't exist + def get(self, *q_objs, **query): + """Retrieve the the matching object raising + :class:`~mongoengine.queryset.MultipleObjectsReturned` or + :class:`~mongoengine.queryset.DoesNotExist` exceptions if multiple or + no results are found. """ - defaults = kwargs.get('defaults', {}) - if kwargs.has_key('defaults'): - del kwargs['defaults'] - - dataset = self.filter(**kwargs) - cnt = dataset.count() - if cnt == 0: - kwargs.update(defaults) - doc = self._document(**kwargs) - doc.save() - return doc - elif cnt == 1: - return dataset.first() - else: - raise MultipleObjectsReturned(u'%d items returned, expected exactly one' % cnt) - - def get(self, **kwargs): - """Retreive exactly one document. Raise DoesNotExist if it's not found. - """ - dataset = self.filter(**kwargs) - cnt = dataset.count() - if cnt == 1: - return dataset.first() - elif cnt > 1: - raise MultipleObjectsReturned(u'%d items returned, expected exactly one' % cnt) + self.__call__(*q_objs, **query) + count = self.count() + if count == 1: + return self[0] + elif count > 1: + message = u'%d items returned, instead of 1' % count + raise MultipleObjectsReturned(message) else: raise DoesNotExist('Document not found') + def get_or_create(self, *q_objs, **query): + """Retreive unique object or create, if it doesn't exist. Raises + :class:`~mongoengine.queryset.MultipleObjectsReturned` if multiple + results are found. A new document will be created if the document + doesn't exists; a dictionary of default values for the new document + may be provided as a keyword argument called :attr:`defaults`. + """ + defaults = query.get('defaults', {}) + if query.has_key('defaults'): + del query['defaults'] + + self.__call__(*q_objs, **query) + count = self.count() + if count == 0: + query.update(defaults) + doc = self._document(**query) + doc.save() + return doc + elif count == 1: + return self.first() + else: + message = u'%d items returned, instead of 1' % count + raise MultipleObjectsReturned(message) + def first(self): """Retrieve the first object matching the query. """ diff --git a/tests/queryset.py b/tests/queryset.py index 1e06615a..00f3e461 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -2,7 +2,8 @@ import unittest import pymongo from datetime import datetime -from mongoengine.queryset import QuerySet +from mongoengine.queryset import (QuerySet, MultipleObjectsReturned, + DoesNotExist) from mongoengine import * @@ -135,6 +136,54 @@ class QuerySetTest(unittest.TestCase): person = self.Person.objects.with_id(person1.id) self.assertEqual(person.name, "User A") + def test_find_only_one(self): + """Ensure that a query using ``get`` returns at most one result. + """ + # Try retrieving when no objects exists + self.assertRaises(DoesNotExist, self.Person.objects.get) + + person1 = self.Person(name="User A", age=20) + person1.save() + person2 = self.Person(name="User B", age=30) + person2.save() + + # Retrieve the first person from the database + self.assertRaises(MultipleObjectsReturned, self.Person.objects.get) + + # Use a query to filter the people found to just person2 + person = self.Person.objects.get(age=30) + self.assertEqual(person.name, "User B") + + person = self.Person.objects.get(age__lt=30) + self.assertEqual(person.name, "User A") + + def test_get_or_create(self): + """Ensure that ``get_or_create`` returns one result or creates a new + document. + """ + person1 = self.Person(name="User A", age=20) + person1.save() + person2 = self.Person(name="User B", age=30) + person2.save() + + # Retrieve the first person from the database + self.assertRaises(MultipleObjectsReturned, + self.Person.objects.get_or_create) + + # Use a query to filter the people found to just person2 + person = self.Person.objects.get_or_create(age=30) + self.assertEqual(person.name, "User B") + + person = self.Person.objects.get_or_create(age__lt=30) + self.assertEqual(person.name, "User A") + + # Try retrieving when no objects exists - new doc should be created + self.Person.objects.get_or_create(age=50, defaults={'name': 'User C'}) + + person = self.Person.objects.get(age=50) + self.assertEqual(person.name, "User C") + + def test_filter_chaining(self): """Ensure filters can be chained together. """