diff --git a/docs/changelog.rst b/docs/changelog.rst index 0d164b51..e24eaf4f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -39,6 +39,7 @@ Changes in 0.8.X - Added switch_collection method to document instances (#220) - Added support for compound primary keys (#149) (#121) - Fixed overriding objects with custom manager (#58) +- Added no_dereference method for querysets (#82) (#61) Changes in 0.7.9 ================ diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 7ccf1432..32798531 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -373,17 +373,22 @@ Turning off dereferencing ------------------------- Sometimes for performance reasons you don't want to automatically dereference -data . To turn off all dereferencing you can use the +data. To turn off dereferencing of the results of a query use +:func:`~mongoengine.queryset.QuerySet.no_dereference` on the queryset like so:: + + post = Post.objects.no_dereference().first() + assert(isinstance(post.author, ObjectId)) + +You can also turn off all dereferencing for a fixed period by using the :class:`~mongoengine.context_managers.no_dereference` context manager:: with no_dereference(Post) as Post: post = Post.objects.first() assert(isinstance(post.author, ObjectId)) -.. note:: + # Outside the context manager dereferencing occurs. + assert(isinstance(post.author, User)) - :class:`~mongoengine.context_managers.no_dereference` only works on the - Default QuerySet manager. Advanced queries ================ diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 9f400618..7c1597e2 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -1,3 +1,4 @@ +import copy import operator from functools import partial @@ -461,9 +462,10 @@ class BaseDocument(object): return cls._meta.get('collection', None) @classmethod - def _from_son(cls, son): + def _from_son(cls, son, _auto_dereference=True): """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 class_name = son.get('_cls', cls._class_name) @@ -480,7 +482,12 @@ class BaseDocument(object): changed_fields = [] errors_dict = {} - for field_name, field in cls._fields.iteritems(): + fields = cls._fields + if not _auto_dereference: + fields = copy.copy(fields) + + for field_name, field in fields.iteritems(): + field._auto_dereference = _auto_dereference if field.db_field in data: value = data[field.db_field] try: @@ -507,6 +514,8 @@ class BaseDocument(object): obj = cls(__auto_convert=False, **data) obj._changed_fields = changed_fields obj._created = False + if not _auto_dereference: + obj._fields = fields return obj @classmethod diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 82981e25..25f86afd 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -168,6 +168,7 @@ class ComplexBaseField(BaseField): (self.field is None or isinstance(self.field, (GenericReferenceField, ReferenceField)))) + self._auto_dereference = instance._fields[self.name]._auto_dereference if not self.__dereference and instance._initialised and dereference: instance._data[self.name] = self._dereference( instance._data.get(self.name), max_depth=1, instance=instance, diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index e73d4a2a..76d5fbfa 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -93,7 +93,8 @@ class switch_collection(object): class no_dereference(object): """ no_dereference context manager. - Turns off all dereferencing in Documents:: + Turns off all dereferencing in Documents for the duration of the context + manager:: with no_dereference(Group) as Group: Group.objects.find() @@ -118,21 +119,12 @@ class no_dereference(object): def __enter__(self): """ change the objects default and _auto_dereference values""" - if 'queryset_class' in self.cls._meta: - raise OperationError("no_dereference context manager only works on" - " default queryset classes") - objects = self.cls.__dict__['objects'] - objects.default = QuerySetNoDeRef - self.cls.objects = objects for field in self.deref_fields: self.cls._fields[field]._auto_dereference = False return self.cls def __exit__(self, t, value, traceback): """ Reset the default and _auto_dereference values""" - objects = self.cls.__dict__['objects'] - objects.default = QuerySet - self.cls.objects = objects for field in self.deref_fields: self.cls._fields[field]._auto_dereference = True return self.cls @@ -145,7 +137,7 @@ class QuerySetNoDeRef(QuerySet): class query_counter(object): - """ Query_counter contextmanager to get the number of queries. """ + """ Query_counter context manager to get the number of queries. """ def __init__(self): """ Construct the query_counter. """ diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 1ccdb650..11e9d3f3 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -777,7 +777,7 @@ class ReferenceField(BaseField): # Get value from document instance if available value = instance._data.get(self.name) - + self._auto_dereference = instance._fields[self.name]._auto_dereference # Dereference DBRefs if self._auto_dereference and isinstance(value, DBRef): value = self.document_type._get_db().dereference(value) @@ -859,7 +859,8 @@ class GenericReferenceField(BaseField): return self value = instance._data.get(self.name) - if isinstance(value, (dict, SON)): + self._auto_dereference = instance._fields[self.name]._auto_dereference + if self._auto_dereference and isinstance(value, (dict, SON)): instance._data[self.name] = self.dereference(value) return super(GenericReferenceField, self).__get__(instance, owner) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index a9ff6e73..f73b0e7c 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -42,6 +42,7 @@ class QuerySet(object): providing :class:`~mongoengine.Document` objects as the results. """ __dereference = False + _auto_dereference = True def __init__(self, document, collection): self._document = document @@ -145,10 +146,12 @@ class QuerySet(object): elif isinstance(key, int): if queryset._scalar: return queryset._get_scalar( - queryset._document._from_son(queryset._cursor[key])) + queryset._document._from_son(queryset._cursor[key], + _auto_dereference=self._auto_dereference)) if queryset._as_pymongo: return queryset._get_as_pymongo(queryset._cursor.next()) - return queryset._document._from_son(queryset._cursor[key]) + return queryset._document._from_son(queryset._cursor[key], + _auto_dereference=self._auto_dereference) raise AttributeError def __repr__(self): @@ -515,7 +518,7 @@ class QuerySet(object): '_where_clause', '_loaded_fields', '_ordering', '_snapshot', '_timeout', '_class_check', '_slave_okay', '_read_preference', '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', - '_limit', '_skip', '_hint') + '_limit', '_skip', '_hint', '_auto_dereference') for prop in copy_props: val = getattr(self, prop) @@ -1135,6 +1138,12 @@ class QuerySet(object): self.__dereference = _import_class('DeReference')() return self.__dereference + def no_dereference(self): + """Turn off any dereferencing.""" + queryset = self.clone() + queryset._auto_dereference = False + return queryset + # Helper Functions def _item_frequencies_map_reduce(self, field, normalize=False): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 0ad30927..c6b7c0e0 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3103,5 +3103,26 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(results[1]['name'], 'Barack Obama') self.assertEqual(results[1]['price'], Decimal('2.22')) + def test_no_dereference(self): + + class Organization(Document): + name = StringField() + + class User(Document): + name = StringField() + organization = ReferenceField(Organization) + + User.drop_collection() + Organization.drop_collection() + + whitehouse = Organization(name="White House").save() + User(name="Bob Dole", organization=whitehouse).save() + + qs = User.objects() + self.assertTrue(isinstance(qs.first().organization, Organization)) + self.assertFalse(isinstance(qs.no_dereference().first().organization, + Organization)) + self.assertTrue(isinstance(qs.first().organization, Organization)) + if __name__ == '__main__': unittest.main()