|
1 | 1 | # coding: utf-8
|
2 | 2 | """Flask-SQLAlchemy-Cache
|
3 | 3 |
|
4 |
| -A SQLAlchemy CachingQuery implementation for Flask, using Flask-Cache. |
| 4 | +A SQLAlchemy CachingQuery implementation for Flask, using Flask-Caching |
5 | 5 |
|
6 | 6 | It is based in SQLAlchemy docs example:
|
7 | 7 | http://docs.sqlalchemy.org/en/latest/orm/examples.html#module-examples.dogpile_caching
|
8 | 8 | """
|
| 9 | +import random |
| 10 | +import time |
9 | 11 | from hashlib import md5
|
10 | 12 |
|
11 | 13 | from sqlalchemy.orm.interfaces import MapperOption
|
@@ -51,16 +53,37 @@ def create_func(): return list(BaseQuery.__iter__(self))
|
51 | 53 |
|
52 | 54 | def _get_cache_plus_key(self):
|
53 | 55 | """Return a cache region plus key."""
|
54 |
| - key = getattr(self, '_cache_key', self.key_from_query()) |
55 |
| - return self._cache.cache, key |
| 56 | + return self._cache.cache, self._get_key() |
| 57 | + |
| 58 | + def _get_key(self, ignore_ns=False): |
| 59 | + """Get the cache key.""" |
| 60 | + key = getattr(self._cache, 'cache_key', None) |
| 61 | + if key is None: |
| 62 | + key = self.key_from_query() |
| 63 | + |
| 64 | + if self._cache.namespace_key and not ignore_ns: |
| 65 | + ns_val = self._cache.cache.get(self._cache.namespace_key) |
| 66 | + if not ns_val: |
| 67 | + # If the namespace key doesn't exist, create it. But start at the current |
| 68 | + # timestamp plus milliseconds and a 3 digit random number to avoid race |
| 69 | + # conditions. |
| 70 | + ns_val = int(time.time() * 1000 + random.randint(0, 999)) |
| 71 | + self._cache.cache.set(self._cache.namespace_key, ns_val) |
| 72 | + key = 'v{}.{}'.format(ns_val, key) |
| 73 | + |
| 74 | + return key |
56 | 75 |
|
57 | 76 | def invalidate(self):
|
58 | 77 | """Invalidate the cache value represented by this Query."""
|
59 | 78 | cache, cache_key = self._get_cache_plus_key()
|
60 | 79 | cache.delete(cache_key)
|
| 80 | + if self._cache.namespace_key: |
| 81 | + cache.incr(self._cache.namespace_key) |
| 82 | + # Also invalidate if the key exists without a namespace version prepended |
| 83 | + cache.delete(self._get_key(ignore_ns=True)) |
61 | 84 |
|
62 |
| - def get_value(self, merge=True, createfunc=None, |
63 |
| - expiration_time=None, ignore_expiration=False): |
| 85 | + def get_value(self, merge=True, createfunc=None, expiration_time=None, |
| 86 | + ignore_expiration=False): |
64 | 87 | """
|
65 | 88 | Return the value from the cache for this query.
|
66 | 89 | """
|
@@ -112,23 +135,30 @@ def key_from_query(self, qualifier=None):
|
112 | 135 |
|
113 | 136 | class _CacheableMapperOption(MapperOption):
|
114 | 137 |
|
115 |
| - def __init__(self, cache, cache_key=None): |
| 138 | + def __init__(self, cache, cache_key=None, namespace_key=None): |
116 | 139 | """
|
117 | 140 | Construct a new `_CacheableMapperOption`.
|
118 | 141 |
|
119 |
| - :param cache: the cache. Should be a Flask-Cache instance. |
| 142 | + :param cache: the cache. Should be a Flask-Caching instance. |
120 | 143 |
|
121 | 144 | :param cache_key: optional. A string cache key that will serve as
|
122 | 145 | the key to the query. Use this if your query has a huge amount of
|
123 | 146 | parameters (such as when using in_()) which correspond more simply to
|
124 | 147 | some other identifier.
|
| 148 | +
|
| 149 | + :param namespace_key: optional. A string cache key that can be used |
| 150 | + to group like cache keys. For example, if you are using offset & limit |
| 151 | + at the db level to paginate rows returned, and a new row is added, |
| 152 | + instead of invalidating all keys individually, only the namespace_key |
| 153 | + needs invalidated by incrementing. |
125 | 154 | """
|
126 | 155 | self.cache = cache
|
127 | 156 | self.cache_key = cache_key
|
| 157 | + self.namespace_key = namespace_key |
128 | 158 |
|
129 | 159 | def __getstate__(self):
|
130 | 160 | """
|
131 |
| - Flask-Cache instance is not picklable because it has references |
| 161 | + Flask-Caching instance is not picklable because it has references |
132 | 162 | to Flask.app. Also, I don't want it cached.
|
133 | 163 | """
|
134 | 164 | d = self.__dict__.copy()
|
|
0 commit comments