queryset.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import six
  2. from mongoengine.errors import OperationError
  3. from mongoengine.queryset.base import (BaseQuerySet, CASCADE, DENY, DO_NOTHING,
  4. NULLIFY, PULL)
  5. __all__ = ('QuerySet', 'QuerySetNoCache', 'DO_NOTHING', 'NULLIFY', 'CASCADE',
  6. 'DENY', 'PULL')
  7. # The maximum number of items to display in a QuerySet.__repr__
  8. REPR_OUTPUT_SIZE = 20
  9. ITER_CHUNK_SIZE = 100
  10. class QuerySet(BaseQuerySet):
  11. """The default queryset, that builds queries and handles a set of results
  12. returned from a query.
  13. Wraps a MongoDB cursor, providing :class:`~mongoengine.Document` objects as
  14. the results.
  15. """
  16. _has_more = True
  17. _len = None
  18. _result_cache = None
  19. def __iter__(self):
  20. """Iteration utilises a results cache which iterates the cursor
  21. in batches of ``ITER_CHUNK_SIZE``.
  22. If ``self._has_more`` the cursor hasn't been exhausted so cache then
  23. batch. Otherwise iterate the result_cache.
  24. """
  25. self._iter = True
  26. if self._has_more:
  27. return self._iter_results()
  28. # iterating over the cache.
  29. return iter(self._result_cache)
  30. def __len__(self):
  31. """Since __len__ is called quite frequently (for example, as part of
  32. list(qs)), we populate the result cache and cache the length.
  33. """
  34. if self._len is not None:
  35. return self._len
  36. # Populate the result cache with *all* of the docs in the cursor
  37. if self._has_more:
  38. list(self._iter_results())
  39. # Cache the length of the complete result cache and return it
  40. self._len = len(self._result_cache)
  41. return self._len
  42. def __repr__(self):
  43. """Provide a string representation of the QuerySet"""
  44. if self._iter:
  45. return '.. queryset mid-iteration ..'
  46. self._populate_cache()
  47. data = self._result_cache[:REPR_OUTPUT_SIZE + 1]
  48. if len(data) > REPR_OUTPUT_SIZE:
  49. data[-1] = '...(remaining elements truncated)...'
  50. return repr(data)
  51. def _iter_results(self):
  52. """A generator for iterating over the result cache.
  53. Also populates the cache if there are more possible results to
  54. yield. Raises StopIteration when there are no more results.
  55. """
  56. if self._result_cache is None:
  57. self._result_cache = []
  58. pos = 0
  59. while True:
  60. # For all positions lower than the length of the current result
  61. # cache, serve the docs straight from the cache w/o hitting the
  62. # database.
  63. # XXX it's VERY important to compute the len within the `while`
  64. # condition because the result cache might expand mid-iteration
  65. # (e.g. if we call len(qs) inside a loop that iterates over the
  66. # queryset). Fortunately len(list) is O(1) in Python, so this
  67. # doesn't cause performance issues.
  68. while pos < len(self._result_cache):
  69. yield self._result_cache[pos]
  70. pos += 1
  71. # return if we already established there were no more
  72. # docs in the db cursor.
  73. if not self._has_more:
  74. return
  75. # Otherwise, populate more of the cache and repeat.
  76. if len(self._result_cache) <= pos:
  77. self._populate_cache()
  78. def _populate_cache(self):
  79. """
  80. Populates the result cache with ``ITER_CHUNK_SIZE`` more entries
  81. (until the cursor is exhausted).
  82. """
  83. if self._result_cache is None:
  84. self._result_cache = []
  85. # Skip populating the cache if we already established there are no
  86. # more docs to pull from the database.
  87. if not self._has_more:
  88. return
  89. # Pull in ITER_CHUNK_SIZE docs from the database and store them in
  90. # the result cache.
  91. try:
  92. for _ in six.moves.range(ITER_CHUNK_SIZE):
  93. self._result_cache.append(six.next(self))
  94. except StopIteration:
  95. # Getting this exception means there are no more docs in the
  96. # db cursor. Set _has_more to False so that we can use that
  97. # information in other places.
  98. self._has_more = False
  99. def count(self, with_limit_and_skip=False):
  100. """Count the selected elements in the query.
  101. :param with_limit_and_skip (optional): take any :meth:`limit` or
  102. :meth:`skip` that has been applied to this cursor into account when
  103. getting the count
  104. """
  105. if with_limit_and_skip is False:
  106. return super(QuerySet, self).count(with_limit_and_skip)
  107. if self._len is None:
  108. self._len = super(QuerySet, self).count(with_limit_and_skip)
  109. return self._len
  110. def no_cache(self):
  111. """Convert to a non-caching queryset
  112. .. versionadded:: 0.8.3 Convert to non caching queryset
  113. """
  114. if self._result_cache is not None:
  115. raise OperationError('QuerySet already cached')
  116. return self._clone_into(QuerySetNoCache(self._document,
  117. self._collection))
  118. class QuerySetNoCache(BaseQuerySet):
  119. """A non caching QuerySet"""
  120. def cache(self):
  121. """Convert to a caching queryset
  122. .. versionadded:: 0.8.3 Convert to caching queryset
  123. """
  124. return self._clone_into(QuerySet(self._document, self._collection))
  125. def __repr__(self):
  126. """Provides the string representation of the QuerySet
  127. .. versionchanged:: 0.6.13 Now doesnt modify the cursor
  128. """
  129. if self._iter:
  130. return '.. queryset mid-iteration ..'
  131. data = []
  132. for _ in six.moves.range(REPR_OUTPUT_SIZE + 1):
  133. try:
  134. data.append(six.next(self))
  135. except StopIteration:
  136. break
  137. if len(data) > REPR_OUTPUT_SIZE:
  138. data[-1] = '...(remaining elements truncated)...'
  139. self.rewind()
  140. return repr(data)
  141. def __iter__(self):
  142. queryset = self
  143. if queryset._iter:
  144. queryset = self.clone()
  145. queryset.rewind()
  146. return queryset