1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104 | from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.core.cache import cache
DEFAULT_CACHE_TIME = 15*60 # 15 minutes
# TODO
# - Come up with a better method for invalidation
# - Add invalidation for count() when a queryset is invalidated
# - Find a way to make AutoCacheManager override `objects` in models
# - Add some handling to allow CacheManager to react differently based on query type (get, count, filter, select_related)
# CacheManager -- A manager to store and retrieve cached objects using CACHE_BACKEND
# (Optional) <string key_prefix> -- the key prefix for all cached objects on this model [default: db_table]
# (Optional) <int timeout> -- in seconds, the maximum time before data is invalidated
# cachemanager.clean() -- Invalidates cached data
# <instance/queryset data> -- the queryset, or instance of the object to be invalidated
class CacheManager(Manager):
def __init__(self, *args, **kwargs):
self.key_prefix = kwargs.pop('key_prefix', None)
self.timeout = kwargs.pop('timeout', None)
Manager.__init__(self)
def get_query_set(self):
return CachedQuerySet(self.model, self.timeout, self.key_prefix)
# clean will accept either a queryset or an instance
def clean(self, data):
# invalidate the .get() request
if isinstance(data, self.model):
self.clean(self.filter(pk=self._get_pk_val()))
elif isinstance(data, CachedQuerySet):
self.clean()
else:
raise TypeError("instance or queryset required for data, got %r" % (data,))
# CachedQuerySet -- Extends the QuerySet object -- additionally adds a .cache() method
# queryset.cache() -- Overrides CacheManager's options for this QuerySet
# (Optional) <string key_prefix> -- the key prefix for all cached objects on this model [default: db_table]
# (Optional) <int timeout> -- in seconds, the maximum time before data is invalidated
# queryset.clean() -- Removes queryset from the cache -- recommended to use cachemanager.clean()
# must be called as the last method of the queryset
# <instance/queryset data> -- the queryset, or instance of the object to be invalidated
class CachedQuerySet(QuerySet):
def __init__(self, model=None, *args, **kwargs):
self.__cache_key = None
self.key_prefix = kwargs.pop('key_prefix', model and model._meta.db_table or '')
self.timeout = kwargs.pop('timeout', getattr(cache, 'default_timeout', DEFAULT_CACHE_TIME))
if not isinstance(self.key_prefix, basestring):
raise TypeError("string required for key_prefix, got %r" % (self.key_prefix,))
if not isinstance(self.timeout, int):
raise TypeError("integer required for timeout, got %r" % (self.timeout,))
QuerySet.__init__(self, model)
def _get_sorted_clause_key(self):
return (isinstance(i, basestring) and i.lower().replace('`', '').replace("'", '') or str(tuple(sorted(i))) for i in self._get_sql_clause())
def _get_cache_key(self, xtra=''):
if not self.__cache_key:
self.__cache_key = self.key_prefix + str(hash(''.join(self._get_sorted_clause_key()))) + xtra
return self.__cache_key
def _get_data(self):
data = cache.get(self._get_cache_key())
if data is None:
data = QuerySet._get_data(self)
cache.set(self._get_cache_key(), [d for d in data], self.timeout)
return data
def count(self):
count = cache.get(self._get_cache_key('count'))
if count is None:
count = int(QuerySet.count(self))
cache.set(self._get_cache_key('count'), count, self.timeout)
return count
def cache(self, *args, **kwargs):
self.key_prefix = kwargs.pop('key_prefix', self.key_prefix)
self.timeout = kwargs.pop('timeout', self.timeout)
if not isinstance(self.key_prefix, basestring):
raise TypeError("string required for key_prefix, got %r" % (self.key_prefix,))
if not isinstance(self.timeout, int):
raise TypeError("integer required for timeout, got %r" % (self.timeout,))
def clean(self):
cache.delete(self._get_cache_key())
# AutoCacheManager -- automatically replaces objects with a CacheManager and
# overrides save/delete functions to update the cache as needed
class AutoCacheManager(object):
# TODO: find a way to make `objects` actually set correctly
objects = CacheManager()
no_cache = Manager()
# TODO: know how to clean this w/o guessing for CacheManager
def delete(self, *args, **kwargs):
self.__class__.objects.clean(self)
Manager.delete(self, *args, **kwargs)
def save(self, *args, **kwargs):
Manager.save(self, *args, **kwargs)
self.__class__.objects.clean(self)
|