diff --git a/django/bin/django-admin.py b/django/bin/django-admin.py index 17c28b81fd..31af89dae5 100755 --- a/django/bin/django-admin.py +++ b/django/bin/django-admin.py @@ -6,6 +6,7 @@ import os, sys 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 @@ ACTION_MAPPING = { '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] diff --git a/django/core/cache.py b/django/core/cache.py index 18ff75f19f..6437766c2c 100644 --- a/django/core/cache.py +++ b/django/core/cache.py @@ -15,10 +15,9 @@ The CACHE_BACKEND setting is a quasi-URI; examples are: 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 @@ arguments are: 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 @@ class _FileCache(_SimpleCache): 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 @@ _BACKENDS = { 'simple' : _SimpleCache, 'locmem' : _LocMemCache, 'file' : _FileCache, + 'db' : _DBCache, } def get_cache(backend_uri): diff --git a/django/core/management.py b/django/core/management.py index 6094430dae..b02914b549 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -621,3 +621,35 @@ def runserver(addr, port): 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]" \ No newline at end of file diff --git a/docs/cache.txt b/docs/cache.txt index 9f50e45511..ff1c406f7c 100644 --- a/docs/cache.txt +++ b/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 diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 3f428161e0..b4b07f4f12 100644 --- a/docs/django-admin.txt +++ b/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 ---------------