Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added a database-backed cache backend, along with a tool in django-ad…

…min to

create the necessary table structure.  This closes #515; thanks again, 
Eugene!


git-svn-id: http://code.djangoproject.com/svn/django/trunk@692 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 0fa1aa8711a6e1f3653e98943f2847366c0ac556 1 parent 6b4095a
@jacobian jacobian authored
View
8 django/bin/django-admin.py
@@ -6,6 +6,7 @@
ACTION_MAPPING = {
'adminindex': management.get_admin_index,
'createsuperuser': management.createsuperuser,
+ 'createcachetable' : management.createcachetable,
# 'dbcheck': management.database_check,
'init': management.init,
'inspectdb': management.inspectdb,
@@ -23,7 +24,7 @@
'validate': management.validate,
}
-NO_SQL_TRANSACTION = ('adminindex', 'dbcheck', 'install', 'sqlindexes')
+NO_SQL_TRANSACTION = ('adminindex', 'createcachetable', 'dbcheck', 'install', 'sqlindexes')
def get_usage():
"""
@@ -79,6 +80,11 @@ def main():
except NotImplementedError:
sys.stderr.write("Error: %r isn't supported for the currently selected database backend.\n" % action)
sys.exit(1)
+ elif action == 'createcachetable':
+ try:
+ ACTION_MAPPING[action](args[1])
+ except IndexError:
+ parser.print_usage_and_exit()
elif action in ('startapp', 'startproject'):
try:
name = args[1]
View
87 django/core/cache.py
@@ -15,10 +15,9 @@
memcached://127.0.0.1:11211/ A memcached backend; the server is running
on localhost port 11211.
- sql://tablename/ A SQL backend. If you use this backend,
- you must have django.contrib.cache in
- INSTALLED_APPS, and you must have installed
- the tables for django.contrib.cache.
+ db://tablename/ A database backend in a table named
+ "tablename". This table should be created
+ with "django-admin createcachetable".
file:///var/tmp/django_cache/ A file-based cache stored in the directory
/var/tmp/django_cache/.
@@ -55,7 +54,7 @@
For example:
memcached://127.0.0.1:11211/?timeout=60
- sql://tablename/?timeout=120&max_entries=500&cull_percentage=4
+ db://tablename/?timeout=120&max_entries=500&cull_percentage=4
Invalid arguments are silently ignored, as are invalid values of known
arguments.
@@ -350,7 +349,84 @@ def _cull(self, filelist):
def _key_to_file(self, key):
return os.path.join(self._dir, urllib.quote_plus(key))
+
+#############
+# SQL cache #
+#############
+
+import base64
+from django.core.db import db
+from datetime import datetime
+
+class _DBCache(_Cache):
+ """SQL cache backend"""
+ def __init__(self, table, params):
+ _Cache.__init__(self, params)
+ self._table = table
+ max_entries = params.get('max_entries', 300)
+ try:
+ self._max_entries = int(max_entries)
+ except (ValueError, TypeError):
+ self._max_entries = 300
+ cull_frequency = params.get('cull_frequency', 3)
+ try:
+ self._cull_frequency = int(cull_frequency)
+ except (ValueError, TypeError):
+ self._cull_frequency = 3
+
+ def get(self, key, default=None):
+ cursor = db.cursor()
+ cursor.execute("SELECT key, value, expires FROM %s WHERE key = %%s" % self._table, [key])
+ row = cursor.fetchone()
+ if row is None:
+ return default
+ now = datetime.now()
+ if row[2] < now:
+ cursor.execute("DELETE FROM %s WHERE key = %%s" % self._table, [key])
+ db.commit()
+ return default
+ return pickle.loads(base64.decodestring(row[1]))
+
+ def set(self, key, value, timeout=None):
+ if timeout is None:
+ timeout = self.default_timeout
+ cursor = db.cursor()
+ cursor.execute("SELECT COUNT(*) FROM %s" % self._table)
+ num = cursor.fetchone()[0]
+ now = datetime.now().replace(microsecond=0)
+ exp = datetime.fromtimestamp(time.time() + timeout).replace(microsecond=0)
+ if num > self._max_entries:
+ self._cull(cursor, now)
+ encoded = base64.encodestring(pickle.dumps(value, 2)).strip()
+ cursor.execute("SELECT key FROM %s WHERE key = %%s" % self._table, [key])
+ if cursor.fetchone():
+ cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE key = %%s" % self._table, [encoded, str(exp), key])
+ else:
+ cursor.execute("INSERT INTO %s (key, value, expires) VALUES (%%s, %%s, %%s)" % self._table, [key, encoded, str(exp)])
+ db.commit()
+
+ def delete(self, key):
+ cursor = db.cursor()
+ cursor.execute("DELETE FROM %s WHERE key = %%s" % self._table, [key])
+ db.commit()
+
+ def has_key(self, key):
+ cursor = db.cursor()
+ cursor.execute("SELECT key FROM %s WHERE key = %%s" % self._table, [key])
+ return cursor.fetchone() is not None
+
+ def _cull(self, cursor, now):
+ if self._cull_frequency == 0:
+ cursor.execute("DELETE FROM %s" % self._table)
+ else:
+ cursor.execute("DELETE FROM %s WHERE expires < %%s" % self._table, [str(now)])
+ cursor.execute("SELECT COUNT(*) FROM %s" % self._table)
+ num = cursor.fetchone()[0]
+ if num > self._max_entries:
+ cursor.execute("SELECT key FROM %s ORDER BY key LIMIT 1 OFFSET %%s" % self._table, [num / self._cull_frequency])
+ cursor.execute("DELETE FROM %s WHERE key < %%s" % self._table, [cursor.fetchone()[0]])
+
##########################################
# Read settings and load a cache backend #
##########################################
@@ -362,6 +438,7 @@ def _key_to_file(self, key):
'simple' : _SimpleCache,
'locmem' : _LocMemCache,
'file' : _FileCache,
+ 'db' : _DBCache,
}
def get_cache(backend_uri):
View
32 django/core/management.py
@@ -621,3 +621,35 @@ def inner_run():
from django.utils import autoreload
autoreload.main(inner_run)
runserver.args = '[optional port number, or ipaddr:port]'
+
+def createcachetable(tablename):
+ "Creates the table needed to use the SQL cache backend"
+ from django.core import db, meta
+ fields = (
+ meta.CharField(name='key', maxlength=255, unique=True, primary_key=True),
+ meta.TextField(name='value'),
+ meta.DateTimeField(name='expires', db_index=True),
+ )
+ table_output = []
+ index_output = []
+ for f in fields:
+ field_output = [f.column, db.DATA_TYPES[f.__class__.__name__] % f.__dict__]
+ field_output.append("%sNULL" % (not f.null and "NOT " or ""))
+ if f.unique:
+ field_output.append("UNIQUE")
+ if f.primary_key:
+ field_output.append("PRIMARY KEY")
+ if f.db_index:
+ unique = f.unique and "UNIQUE " or ""
+ index_output.append("CREATE %sINDEX %s_%s ON %s (%s);" % (unique, tablename, f.column, tablename, f.column))
+ table_output.append(" ".join(field_output))
+ full_statement = ["CREATE TABLE %s (" % tablename]
+ for i, line in enumerate(table_output):
+ full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or ''))
+ full_statement.append(');')
+ curs = db.db.cursor()
+ curs.execute("\n".join(full_statement))
+ for statement in index_output:
+ curs.execute(statement)
+ db.db.commit()
+createcachetable.args = "[tablename]"
View
10 docs/cache.txt
@@ -29,10 +29,20 @@ Examples:
memcached://127.0.0.1:11211/ A memcached backend; the server is running
on localhost port 11211.
+ db://tablename/ A database backend in a table named
+ "tablename". This table should be created
+ with "django-admin createcachetable".
+
+ file:///var/tmp/django_cache/ A file-based cache stored in the directory
+ /var/tmp/django_cache/.
+
simple:/// A simple single-process memory cache; you
probably don't want to use this except for
testing. Note that this cache backend is
NOT threadsafe!
+
+ locmem:/// A more sophisticaed local memory cache;
+ this is multi-process- and thread-safe.
============================== ===========================================
All caches may take arguments -- they're given in query-string style. Valid
View
8 docs/django-admin.txt
@@ -40,6 +40,14 @@ your admin's index page. See `Tutorial 2`_ for more information.
.. _Tutorial 2: http://www.djangoproject.com/documentation/tutorial2/
+createcachetable [tablename]
+----------------------------
+
+Creates a cache table named ``tablename`` for use with the database cache
+backend. See the `cache documentation`_ for more information.
+
+.. _cache documentation: http://www.djangoproject.com/documentation/cache/
+
createsuperuser
---------------
Please sign in to comment.
Something went wrong with that request. Please try again.