Cleaner ordering code + allow clearing an explicit sort

This commit is contained in:
Stefan Wojcik 2017-02-07 23:38:56 -08:00
parent 0e77efb0c7
commit bad4e6846a
2 changed files with 97 additions and 39 deletions

View File

@ -971,13 +971,31 @@ class BaseQuerySet(object):
def order_by(self, *keys): def order_by(self, *keys):
"""Order the :class:`~mongoengine.queryset.QuerySet` by the keys. The """Order the :class:`~mongoengine.queryset.QuerySet` by the keys. The
order may be specified by prepending each of the keys by a + or a -. order may be specified by prepending each of the keys by a + or a -.
Ascending order is assumed. Ascending order is assumed. If no keys are passed, existing ordering
is cleared instead.
:param keys: fields to order the query results by; keys may be :param keys: fields to order the query results by; keys may be
prefixed with **+** or **-** to determine the ordering direction prefixed with **+** or **-** to determine the ordering direction
""" """
queryset = self.clone() queryset = self.clone()
queryset._ordering = queryset._get_order_by(keys)
old_ordering = queryset._ordering
new_ordering = queryset._get_order_by(keys)
if queryset._cursor_obj:
# If a cursor object has already been created, apply the sort to it
if new_ordering:
queryset._cursor_obj.sort(new_ordering)
# If we're trying to clear a previous explicit ordering, we need
# to clear the cursor entirely (because PyMongo doesn't allow
# clearing an existing sort on a cursor).
elif old_ordering:
queryset._cursor_obj = None
queryset._ordering = new_ordering
return queryset return queryset
def comment(self, text): def comment(self, text):
@ -1423,10 +1441,13 @@ class BaseQuerySet(object):
raise StopIteration raise StopIteration
raw_doc = self._cursor.next() raw_doc = self._cursor.next()
if self._as_pymongo: if self._as_pymongo:
return self._get_as_pymongo(raw_doc) return self._get_as_pymongo(raw_doc)
doc = self._document._from_son(raw_doc,
_auto_dereference=self._auto_dereference, only_fields=self.only_fields) doc = self._document._from_son(
raw_doc, _auto_dereference=self._auto_dereference,
only_fields=self.only_fields)
if self._scalar: if self._scalar:
return self._get_scalar(doc) return self._get_scalar(doc)
@ -1436,7 +1457,6 @@ class BaseQuerySet(object):
def rewind(self): def rewind(self):
"""Rewind the cursor to its unevaluated state. """Rewind the cursor to its unevaluated state.
.. versionadded:: 0.3 .. versionadded:: 0.3
""" """
self._iter = False self._iter = False
@ -1486,43 +1506,54 @@ class BaseQuerySet(object):
@property @property
def _cursor(self): def _cursor(self):
if self._cursor_obj is None: """Return a PyMongo cursor object corresponding to this queryset."""
# In PyMongo 3+, we define the read preference on a collection # If _cursor_obj already exists, return it immediately.
# level, not a cursor level. Thus, we need to get a cloned if self._cursor_obj is not None:
# collection object using `with_options` first. return self._cursor_obj
if IS_PYMONGO_3 and self._read_preference is not None:
self._cursor_obj = self._collection\
.with_options(read_preference=self._read_preference)\
.find(self._query, **self._cursor_args)
else:
self._cursor_obj = self._collection.find(self._query,
**self._cursor_args)
# Apply where clauses to cursor
if self._where_clause:
where_clause = self._sub_js_fields(self._where_clause)
self._cursor_obj.where(where_clause)
if self._ordering: # Create a new PyMongo cursor.
# Apply query ordering # XXX In PyMongo 3+, we define the read preference on a collection
self._cursor_obj.sort(self._ordering) # level, not a cursor level. Thus, we need to get a cloned collection
elif self._ordering is None and self._document._meta['ordering']: # object using `with_options` first.
# Otherwise, apply the ordering from the document model, unless if IS_PYMONGO_3 and self._read_preference is not None:
# it's been explicitly cleared via order_by with no arguments self._cursor_obj = self._collection\
order = self._get_order_by(self._document._meta['ordering']) .with_options(read_preference=self._read_preference)\
self._cursor_obj.sort(order) .find(self._query, **self._cursor_args)
else:
self._cursor_obj = self._collection.find(self._query,
**self._cursor_args)
# Apply "where" clauses to cursor
if self._where_clause:
where_clause = self._sub_js_fields(self._where_clause)
self._cursor_obj.where(where_clause)
if self._limit is not None: # Apply ordering to the cursor.
self._cursor_obj.limit(self._limit) # XXX self._ordering can be equal to:
# * None if we didn't explicitly call order_by on this queryset.
# * A list of PyMongo-style sorting tuples.
# * An empty list if we explicitly called order_by() without any
# arguments. This indicates that we want to clear the default
# ordering.
if self._ordering:
# explicit ordering
self._cursor_obj.sort(self._ordering)
elif self._ordering is None and self._document._meta['ordering']:
# default ordering
order = self._get_order_by(self._document._meta['ordering'])
self._cursor_obj.sort(order)
if self._skip is not None: if self._limit is not None:
self._cursor_obj.skip(self._skip) self._cursor_obj.limit(self._limit)
if self._hint != -1: if self._skip is not None:
self._cursor_obj.hint(self._hint) self._cursor_obj.skip(self._skip)
if self._batch_size is not None: if self._hint != -1:
self._cursor_obj.batch_size(self._batch_size) self._cursor_obj.hint(self._hint)
if self._batch_size is not None:
self._cursor_obj.batch_size(self._batch_size)
return self._cursor_obj return self._cursor_obj
@ -1696,8 +1727,15 @@ class BaseQuerySet(object):
raise err raise err
return ret return ret
def _get_order_by(self, keys): def _get_order_by(self, keys):
"""Creates a list of order by fields""" """Given a list of MongoEngine-style sort keys, return a list
of sorting tuples that can be applied to a PyMongo cursor. For
example:
>>> qs._get_order_by(['-last_name', 'first_name'])
[('last_name', -1), ('first_name', 1)]
"""
key_list = [] key_list = []
for key in keys: for key in keys:
if not key: if not key:
@ -1710,17 +1748,19 @@ class BaseQuerySet(object):
direction = pymongo.ASCENDING direction = pymongo.ASCENDING
if key[0] == '-': if key[0] == '-':
direction = pymongo.DESCENDING direction = pymongo.DESCENDING
if key[0] in ('-', '+'): if key[0] in ('-', '+'):
key = key[1:] key = key[1:]
key = key.replace('__', '.') key = key.replace('__', '.')
try: try:
key = self._document._translate_field_name(key) key = self._document._translate_field_name(key)
except Exception: except Exception:
# TODO this exception should be more specific
pass pass
key_list.append((key, direction)) key_list.append((key, direction))
if self._cursor_obj and key_list:
self._cursor_obj.sort(key_list)
return key_list return key_list
def _get_scalar(self, doc): def _get_scalar(self, doc):

View File

@ -1242,6 +1242,7 @@ class QuerySetTest(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
# default ordering should be used by default
with db_ops_tracker() as q: with db_ops_tracker() as q:
BlogPost.objects.filter(title='whatever').first() BlogPost.objects.filter(title='whatever').first()
self.assertEqual(len(q.get_ops()), 1) self.assertEqual(len(q.get_ops()), 1)
@ -1250,11 +1251,28 @@ class QuerySetTest(unittest.TestCase):
{'published_date': -1} {'published_date': -1}
) )
# calling order_by() should clear the default ordering
with db_ops_tracker() as q: with db_ops_tracker() as q:
BlogPost.objects.filter(title='whatever').order_by().first() BlogPost.objects.filter(title='whatever').order_by().first()
self.assertEqual(len(q.get_ops()), 1) self.assertEqual(len(q.get_ops()), 1)
self.assertFalse('$orderby' in q.get_ops()[0]['query']) self.assertFalse('$orderby' in q.get_ops()[0]['query'])
# calling an explicit order_by should use a specified sort
with db_ops_tracker() as q:
BlogPost.objects.filter(title='whatever').order_by('published_date').first()
self.assertEqual(len(q.get_ops()), 1)
self.assertEqual(
q.get_ops()[0]['query']['$orderby'],
{'published_date': 1}
)
# calling order_by() after an explicit sort should clear it
with db_ops_tracker() as q:
qs = BlogPost.objects.filter(title='whatever').order_by('published_date')
qs.order_by().first()
self.assertEqual(len(q.get_ops()), 1)
self.assertFalse('$orderby' in q.get_ops()[0]['query'])
def test_no_ordering_for_get(self): def test_no_ordering_for_get(self):
""" Ensure that Doc.objects.get doesn't use any ordering. """ Ensure that Doc.objects.get doesn't use any ordering.
""" """