Permalink
Browse files

caching for queryset.count()

  • Loading branch information...
1 parent d4c17ff commit c1e871f4d7360e759596d9bb620a48016651c301 @jbalogh jbalogh committed Feb 18, 2010
Showing with 51 additions and 5 deletions.
  1. +14 −2 caching/base.py
  2. +9 −2 docs/index.rst
  3. +1 −1 fabfile.py
  4. +1 −0 requirements.txt
  5. +26 −0 tests/test_cache.py
View
@@ -143,15 +143,27 @@ def add_to_flush_list(flush_keys, new_key):
class CachingQuerySet(models.query.QuerySet):
+ def query_key(self):
+ sql, params = self.query.get_compiler(using=self.db).as_sql()
+ return sql % params
+
def iterator(self):
# Work-around for Django #12717.
- sql, params = self.query.get_compiler(using=self.db).as_sql()
- query_string = sql % params
+ query_string = self.query_key()
iterator = super(CachingQuerySet, self).iterator
for obj in CacheMachine(query_string, iterator):
yield obj
raise StopIteration
+ def count(self):
+ timeout = getattr(settings, 'CACHE_COUNT_TIMEOUT', None)
+ super_count = super(CachingQuerySet, self).count
+ query_string = 'count:%s' % self.query_key()
+ if timeout is None:
+ return super_count()
+ else:
+ return cached(super_count, query_string, timeout)
+
def flush_key(obj):
"""We put flush lists in the flush: namespace."""
View
@@ -36,6 +36,14 @@ If you want to set a prefix for all keys in Cache Machine, define
CACHE_PREFIX = 'weee:'
+Calls to ``QuerySet.count()`` can be cached, but they cannot be reliably
+invalidated. Cache Machine would have to do a full select to figure out the
+object keys, which is probably much more data than you want to pull. I
+recommend a short cache timeout; long enough to avoid repetitive queries, but
+short enough that stale counts won't be a big deal. ::
+
+ CACHE_COUNT_TIMEOUT = 60 # seconds, not too long.
+
Cache Manager
-------------
@@ -71,8 +79,7 @@ once iteration is done.
Caching is supported for normal :class:`QuerySets <django.db.models.QuerySet>` and
for :meth:`django.db.models.Manager.raw`. At this time, caching has not been
-implemented for ``QuerySet.count``, ``QuerySet.values``, or
-``QuerySet.values_list``.
+implemented for ``QuerySet.values`` or ``QuerySet.values_list``.
To support easy cache invalidation, we use "flush lists" to mark the cached
queries an object belongs to. That way, all queries where an object was found
View
@@ -28,7 +28,7 @@ def doc(kind='html'):
def test():
- local('django-admin.py test')
+ local('django-admin.py test -s')
def updoc():
View
@@ -1,5 +1,6 @@
# These are the reqs to build docs and run tests.
sphinx
+mock
-e git://github.com/jbalogh/django-nose.git@6f060d49ee193a05734704820f3fea92ee1759d2#egg=django-nose
-e svn+http://code.djangoproject.com/svn/django/trunk@12335#egg=Django
python-memcached
View
@@ -1,5 +1,7 @@
+from django.conf import settings
from django.core.cache import cache
+import mock
from nose.tools import eq_
from test_utils import ExtraAppTestCase
@@ -14,6 +16,10 @@ class CachingTestCase(ExtraAppTestCase):
def setUp(self):
cache.clear()
+ self.old_timeout = getattr(settings, 'CACHE_COUNT_TIMEOUT', None)
+
+ def tearDown(self):
+ settings.CACHE_COUNT_TIMEOUT = self.old_timeout
def test_flush_key(self):
"""flush_key should work for objects or strings."""
@@ -97,3 +103,23 @@ def test_raw_cache_params(self):
raw2 = list(Addon.objects.raw(sql, [2]))[0]
eq_(raw2.id, 2)
+
+ @mock.patch('caching.base.cache')
+ def test_count_cache(self, cache_mock):
+ settings.CACHE_COUNT_TIMEOUT = 60
+ cache_mock.scheme = 'memcached'
+ cache_mock.get.return_value = None
+
+ q = Addon.objects.all()
+ count = q.count()
+
+ args, kwargs = cache_mock.set.call_args
+ key, value, timeout = args
+ eq_(value, 2)
+ eq_(timeout, 60)
+
+ @mock.patch('caching.base.cached')
+ def test_count_none_timeout(self, cached_mock):
+ settings.CACHE_COUNT_TIMEOUT = None
+ Addon.objects.count()
+ eq_(cached_mock.call_count, 0)

0 comments on commit c1e871f

Please sign in to comment.