Added a database-backed cache backend, along with a tool in django-admin 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
This commit is contained in:
Jacob Kaplan-Moss 2005-09-25 22:03:30 +00:00
parent 6b4095ad10
commit 0fa1aa8711
5 changed files with 139 additions and 6 deletions

View File

@ -6,6 +6,7 @@ import os, sys
ACTION_MAPPING = { ACTION_MAPPING = {
'adminindex': management.get_admin_index, 'adminindex': management.get_admin_index,
'createsuperuser': management.createsuperuser, 'createsuperuser': management.createsuperuser,
'createcachetable' : management.createcachetable,
# 'dbcheck': management.database_check, # 'dbcheck': management.database_check,
'init': management.init, 'init': management.init,
'inspectdb': management.inspectdb, 'inspectdb': management.inspectdb,
@ -23,7 +24,7 @@ ACTION_MAPPING = {
'validate': management.validate, 'validate': management.validate,
} }
NO_SQL_TRANSACTION = ('adminindex', 'dbcheck', 'install', 'sqlindexes') NO_SQL_TRANSACTION = ('adminindex', 'createcachetable', 'dbcheck', 'install', 'sqlindexes')
def get_usage(): def get_usage():
""" """
@ -79,6 +80,11 @@ def main():
except NotImplementedError: except NotImplementedError:
sys.stderr.write("Error: %r isn't supported for the currently selected database backend.\n" % action) sys.stderr.write("Error: %r isn't supported for the currently selected database backend.\n" % action)
sys.exit(1) sys.exit(1)
elif action == 'createcachetable':
try:
ACTION_MAPPING[action](args[1])
except IndexError:
parser.print_usage_and_exit()
elif action in ('startapp', 'startproject'): elif action in ('startapp', 'startproject'):
try: try:
name = args[1] name = args[1]

View File

@ -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 memcached://127.0.0.1:11211/ A memcached backend; the server is running
on localhost port 11211. on localhost port 11211.
sql://tablename/ A SQL backend. If you use this backend, db://tablename/ A database backend in a table named
you must have django.contrib.cache in "tablename". This table should be created
INSTALLED_APPS, and you must have installed with "django-admin createcachetable".
the tables for django.contrib.cache.
file:///var/tmp/django_cache/ A file-based cache stored in the directory file:///var/tmp/django_cache/ A file-based cache stored in the directory
/var/tmp/django_cache/. /var/tmp/django_cache/.
@ -55,7 +54,7 @@ arguments are:
For example: For example:
memcached://127.0.0.1:11211/?timeout=60 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 Invalid arguments are silently ignored, as are invalid values of known
arguments. arguments.
@ -351,6 +350,83 @@ class _FileCache(_SimpleCache):
def _key_to_file(self, key): def _key_to_file(self, key):
return os.path.join(self._dir, urllib.quote_plus(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 # # Read settings and load a cache backend #
########################################## ##########################################
@ -362,6 +438,7 @@ _BACKENDS = {
'simple' : _SimpleCache, 'simple' : _SimpleCache,
'locmem' : _LocMemCache, 'locmem' : _LocMemCache,
'file' : _FileCache, 'file' : _FileCache,
'db' : _DBCache,
} }
def get_cache(backend_uri): def get_cache(backend_uri):

View File

@ -621,3 +621,35 @@ def runserver(addr, port):
from django.utils import autoreload from django.utils import autoreload
autoreload.main(inner_run) autoreload.main(inner_run)
runserver.args = '[optional port number, or ipaddr:port]' 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 File

@ -29,10 +29,20 @@ Examples:
memcached://127.0.0.1:11211/ A memcached backend; the server is running memcached://127.0.0.1:11211/ A memcached backend; the server is running
on localhost port 11211. 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 simple:/// A simple single-process memory cache; you
probably don't want to use this except for probably don't want to use this except for
testing. Note that this cache backend is testing. Note that this cache backend is
NOT threadsafe! 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 All caches may take arguments -- they're given in query-string style. Valid

View File

@ -40,6 +40,14 @@ your admin's index page. See `Tutorial 2`_ for more information.
.. _Tutorial 2: http://www.djangoproject.com/documentation/tutorial2/ .. _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 createsuperuser
--------------- ---------------