db.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. "Database cache backend."
  2. import base64
  3. import time
  4. from datetime import datetime
  5. try:
  6. from django.utils.six.moves import cPickle as pickle
  7. except ImportError:
  8. import pickle
  9. from django.conf import settings
  10. from django.core.cache.backends.base import BaseCache
  11. from django.db import connections, transaction, router, DatabaseError
  12. from django.utils import timezone, six
  13. from django.utils.encoding import force_bytes
  14. class Options(object):
  15. """A class that will quack like a Django model _meta class.
  16. This allows cache operations to be controlled by the router
  17. """
  18. def __init__(self, table):
  19. self.db_table = table
  20. self.app_label = 'django_cache'
  21. self.model_name = 'cacheentry'
  22. self.verbose_name = 'cache entry'
  23. self.verbose_name_plural = 'cache entries'
  24. self.object_name = 'CacheEntry'
  25. self.abstract = False
  26. self.managed = True
  27. self.proxy = False
  28. class BaseDatabaseCache(BaseCache):
  29. def __init__(self, table, params):
  30. BaseCache.__init__(self, params)
  31. self._table = table
  32. class CacheEntry(object):
  33. _meta = Options(table)
  34. self.cache_model_class = CacheEntry
  35. class DatabaseCache(BaseDatabaseCache):
  36. # This class uses cursors provided by the database connection. This means
  37. # it reads expiration values as aware or naive datetimes depending on the
  38. # value of USE_TZ. They must be compared to aware or naive representations
  39. # of "now" respectively.
  40. # But it bypasses the ORM for write operations. As a consequence, aware
  41. # datetimes aren't made naive for databases that don't support time zones.
  42. # We work around this problem by always using naive datetimes when writing
  43. # expiration values, in UTC when USE_TZ = True and in local time otherwise.
  44. def get(self, key, default=None, version=None):
  45. key = self.make_key(key, version=version)
  46. self.validate_key(key)
  47. db = router.db_for_read(self.cache_model_class)
  48. table = connections[db].ops.quote_name(self._table)
  49. cursor = connections[db].cursor()
  50. cursor.execute("SELECT cache_key, value, expires FROM %s "
  51. "WHERE cache_key = %%s" % table, [key])
  52. row = cursor.fetchone()
  53. if row is None:
  54. return default
  55. now = timezone.now()
  56. if row[2] < now:
  57. db = router.db_for_write(self.cache_model_class)
  58. cursor = connections[db].cursor()
  59. cursor.execute("DELETE FROM %s "
  60. "WHERE cache_key = %%s" % table, [key])
  61. return default
  62. value = connections[db].ops.process_clob(row[1])
  63. return pickle.loads(base64.b64decode(force_bytes(value)))
  64. def set(self, key, value, timeout=None, version=None):
  65. key = self.make_key(key, version=version)
  66. self.validate_key(key)
  67. self._base_set('set', key, value, timeout)
  68. def add(self, key, value, timeout=None, version=None):
  69. key = self.make_key(key, version=version)
  70. self.validate_key(key)
  71. return self._base_set('add', key, value, timeout)
  72. def _base_set(self, mode, key, value, timeout=None):
  73. if timeout is None:
  74. timeout = self.default_timeout
  75. db = router.db_for_write(self.cache_model_class)
  76. table = connections[db].ops.quote_name(self._table)
  77. cursor = connections[db].cursor()
  78. cursor.execute("SELECT COUNT(*) FROM %s" % table)
  79. num = cursor.fetchone()[0]
  80. now = timezone.now()
  81. now = now.replace(microsecond=0)
  82. if settings.USE_TZ:
  83. exp = datetime.utcfromtimestamp(time.time() + timeout)
  84. else:
  85. exp = datetime.fromtimestamp(time.time() + timeout)
  86. exp = exp.replace(microsecond=0)
  87. if num > self._max_entries:
  88. self._cull(db, cursor, now)
  89. pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
  90. b64encoded = base64.b64encode(pickled)
  91. # The DB column is expecting a string, so make sure the value is a
  92. # string, not bytes. Refs #19274.
  93. if six.PY3:
  94. b64encoded = b64encoded.decode('latin1')
  95. try:
  96. with transaction.atomic(using=db):
  97. cursor.execute("SELECT cache_key, expires FROM %s "
  98. "WHERE cache_key = %%s" % table, [key])
  99. result = cursor.fetchone()
  100. exp = connections[db].ops.value_to_db_datetime(exp)
  101. if result and (mode == 'set' or (mode == 'add' and result[1] < now)):
  102. cursor.execute("UPDATE %s SET value = %%s, expires = %%s "
  103. "WHERE cache_key = %%s" % table,
  104. [b64encoded, exp, key])
  105. else:
  106. cursor.execute("INSERT INTO %s (cache_key, value, expires) "
  107. "VALUES (%%s, %%s, %%s)" % table,
  108. [key, b64encoded, exp])
  109. except DatabaseError:
  110. # To be threadsafe, updates/inserts are allowed to fail silently
  111. return False
  112. else:
  113. return True
  114. def delete(self, key, version=None):
  115. key = self.make_key(key, version=version)
  116. self.validate_key(key)
  117. db = router.db_for_write(self.cache_model_class)
  118. table = connections[db].ops.quote_name(self._table)
  119. cursor = connections[db].cursor()
  120. cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key])
  121. def has_key(self, key, version=None):
  122. key = self.make_key(key, version=version)
  123. self.validate_key(key)
  124. db = router.db_for_read(self.cache_model_class)
  125. table = connections[db].ops.quote_name(self._table)
  126. cursor = connections[db].cursor()
  127. if settings.USE_TZ:
  128. now = datetime.utcnow()
  129. else:
  130. now = datetime.now()
  131. now = now.replace(microsecond=0)
  132. cursor.execute("SELECT cache_key FROM %s "
  133. "WHERE cache_key = %%s and expires > %%s" % table,
  134. [key, connections[db].ops.value_to_db_datetime(now)])
  135. return cursor.fetchone() is not None
  136. def _cull(self, db, cursor, now):
  137. if self._cull_frequency == 0:
  138. self.clear()
  139. else:
  140. # When USE_TZ is True, 'now' will be an aware datetime in UTC.
  141. now = now.replace(tzinfo=None)
  142. table = connections[db].ops.quote_name(self._table)
  143. cursor.execute("DELETE FROM %s WHERE expires < %%s" % table,
  144. [connections[db].ops.value_to_db_datetime(now)])
  145. cursor.execute("SELECT COUNT(*) FROM %s" % table)
  146. num = cursor.fetchone()[0]
  147. if num > self._max_entries:
  148. cull_num = num // self._cull_frequency
  149. cursor.execute(
  150. connections[db].ops.cache_key_culling_sql() % table,
  151. [cull_num])
  152. cursor.execute("DELETE FROM %s "
  153. "WHERE cache_key < %%s" % table,
  154. [cursor.fetchone()[0]])
  155. def clear(self):
  156. db = router.db_for_write(self.cache_model_class)
  157. table = connections[db].ops.quote_name(self._table)
  158. cursor = connections[db].cursor()
  159. cursor.execute('DELETE FROM %s' % table)
  160. # For backwards compatibility
  161. class CacheClass(DatabaseCache):
  162. pass