Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
Jacob Kaplan-Moss authored September 25, 2005
8  django/bin/django-admin.py
@@ -6,6 +6,7 @@
6 6
 ACTION_MAPPING = {
7 7
     'adminindex': management.get_admin_index,
8 8
     'createsuperuser': management.createsuperuser,
  9
+    'createcachetable' : management.createcachetable,
9 10
 #     'dbcheck': management.database_check,
10 11
     'init': management.init,
11 12
     'inspectdb': management.inspectdb,
@@ -23,7 +24,7 @@
23 24
     'validate': management.validate,
24 25
 }
25 26
 
26  
-NO_SQL_TRANSACTION = ('adminindex', 'dbcheck', 'install', 'sqlindexes')
  27
+NO_SQL_TRANSACTION = ('adminindex', 'createcachetable', 'dbcheck', 'install', 'sqlindexes')
27 28
 
28 29
 def get_usage():
29 30
     """
@@ -79,6 +80,11 @@ def main():
79 80
         except NotImplementedError:
80 81
             sys.stderr.write("Error: %r isn't supported for the currently selected database backend.\n" % action)
81 82
             sys.exit(1)
  83
+    elif action == 'createcachetable':
  84
+        try:
  85
+            ACTION_MAPPING[action](args[1])
  86
+        except IndexError:
  87
+            parser.print_usage_and_exit()
82 88
     elif action in ('startapp', 'startproject'):
83 89
         try:
84 90
             name = args[1]
87  django/core/cache.py
@@ -15,10 +15,9 @@
15 15
     memcached://127.0.0.1:11211/    A memcached backend; the server is running
16 16
                                     on localhost port 11211.
17 17
 
18  
-    sql://tablename/                A SQL backend.  If you use this backend,
19  
-                                    you must have django.contrib.cache in
20  
-                                    INSTALLED_APPS, and you must have installed
21  
-                                    the tables for django.contrib.cache.
  18
+    db://tablename/                 A database backend in a table named 
  19
+                                    "tablename". This table should be created
  20
+                                    with "django-admin createcachetable".
22 21
 
23 22
     file:///var/tmp/django_cache/   A file-based cache stored in the directory
24 23
                                     /var/tmp/django_cache/.
@@ -55,7 +54,7 @@
55 54
 For example:
56 55
 
57 56
     memcached://127.0.0.1:11211/?timeout=60
58  
-    sql://tablename/?timeout=120&max_entries=500&cull_percentage=4
  57
+    db://tablename/?timeout=120&max_entries=500&cull_percentage=4
59 58
 
60 59
 Invalid arguments are silently ignored, as are invalid values of known
61 60
 arguments.
@@ -350,7 +349,84 @@ def _cull(self, filelist):
350 349
       
351 350
     def _key_to_file(self, key):
352 351
         return os.path.join(self._dir, urllib.quote_plus(key))
  352
+
  353
+#############
  354
+# SQL cache #
  355
+#############
  356
+
  357
+import base64
  358
+from django.core.db import db
  359
+from datetime import datetime
  360
+
  361
+class _DBCache(_Cache):
  362
+    """SQL cache backend"""
353 363
     
  364
+    def __init__(self, table, params):
  365
+        _Cache.__init__(self, params)
  366
+        self._table = table
  367
+        max_entries = params.get('max_entries', 300) 
  368
+        try: 
  369
+            self._max_entries = int(max_entries) 
  370
+        except (ValueError, TypeError): 
  371
+            self._max_entries = 300 
  372
+        cull_frequency = params.get('cull_frequency', 3) 
  373
+        try: 
  374
+            self._cull_frequency = int(cull_frequency) 
  375
+        except (ValueError, TypeError): 
  376
+            self._cull_frequency = 3 
  377
+        
  378
+    def get(self, key, default=None):
  379
+        cursor = db.cursor()
  380
+        cursor.execute("SELECT key, value, expires FROM %s WHERE key = %%s" % self._table, [key])
  381
+        row = cursor.fetchone()
  382
+        if row is None:
  383
+            return default
  384
+        now = datetime.now()
  385
+        if row[2] < now:
  386
+            cursor.execute("DELETE FROM %s WHERE key = %%s" % self._table, [key])
  387
+            db.commit()
  388
+            return default
  389
+        return pickle.loads(base64.decodestring(row[1]))
  390
+        
  391
+    def set(self, key, value, timeout=None):
  392
+        if timeout is None:
  393
+            timeout = self.default_timeout
  394
+        cursor = db.cursor()
  395
+        cursor.execute("SELECT COUNT(*) FROM %s" % self._table)
  396
+        num = cursor.fetchone()[0]
  397
+        now = datetime.now().replace(microsecond=0)
  398
+        exp = datetime.fromtimestamp(time.time() + timeout).replace(microsecond=0)
  399
+        if num > self._max_entries:
  400
+            self._cull(cursor, now)
  401
+        encoded = base64.encodestring(pickle.dumps(value, 2)).strip()
  402
+        cursor.execute("SELECT key FROM %s WHERE key = %%s" % self._table, [key])
  403
+        if cursor.fetchone():
  404
+            cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE key = %%s" % self._table, [encoded, str(exp), key])
  405
+        else:
  406
+            cursor.execute("INSERT INTO %s (key, value, expires) VALUES (%%s, %%s, %%s)" % self._table, [key, encoded, str(exp)])
  407
+        db.commit()
  408
+        
  409
+    def delete(self, key):
  410
+        cursor = db.cursor()
  411
+        cursor.execute("DELETE FROM %s WHERE key = %%s" % self._table, [key])
  412
+        db.commit()
  413
+        
  414
+    def has_key(self, key):
  415
+        cursor = db.cursor()
  416
+        cursor.execute("SELECT key FROM %s WHERE key = %%s" % self._table, [key])
  417
+        return cursor.fetchone() is not None
  418
+        
  419
+    def _cull(self, cursor, now):
  420
+        if self._cull_frequency == 0:
  421
+            cursor.execute("DELETE FROM %s" % self._table)
  422
+        else:
  423
+            cursor.execute("DELETE FROM %s WHERE expires < %%s" % self._table, [str(now)])
  424
+            cursor.execute("SELECT COUNT(*) FROM %s" % self._table)
  425
+            num = cursor.fetchone()[0]
  426
+            if num > self._max_entries:
  427
+                cursor.execute("SELECT key FROM %s ORDER BY key LIMIT 1 OFFSET %%s" % self._table, [num / self._cull_frequency])
  428
+                cursor.execute("DELETE FROM %s WHERE key < %%s" % self._table, [cursor.fetchone()[0]])
  429
+        
354 430
 ##########################################
355 431
 # Read settings and load a cache backend #
356 432
 ##########################################
@@ -362,6 +438,7 @@ def _key_to_file(self, key):
362 438
     'simple'    : _SimpleCache,
363 439
     'locmem'    : _LocMemCache,
364 440
     'file'      : _FileCache,
  441
+    'db'        : _DBCache,
365 442
 }
366 443
 
367 444
 def get_cache(backend_uri):
32  django/core/management.py
@@ -621,3 +621,35 @@ def inner_run():
621 621
     from django.utils import autoreload
622 622
     autoreload.main(inner_run)
623 623
 runserver.args = '[optional port number, or ipaddr:port]'
  624
+
  625
+def createcachetable(tablename):
  626
+    "Creates the table needed to use the SQL cache backend"
  627
+    from django.core import db, meta
  628
+    fields = (
  629
+        meta.CharField(name='key', maxlength=255, unique=True, primary_key=True),
  630
+        meta.TextField(name='value'),
  631
+        meta.DateTimeField(name='expires', db_index=True),
  632
+    )
  633
+    table_output = []
  634
+    index_output = []
  635
+    for f in fields:
  636
+        field_output = [f.column, db.DATA_TYPES[f.__class__.__name__] % f.__dict__]
  637
+        field_output.append("%sNULL" % (not f.null and "NOT " or ""))
  638
+        if f.unique:
  639
+            field_output.append("UNIQUE")
  640
+        if f.primary_key:
  641
+            field_output.append("PRIMARY KEY")
  642
+        if f.db_index:
  643
+            unique = f.unique and "UNIQUE " or ""
  644
+            index_output.append("CREATE %sINDEX %s_%s ON %s (%s);" % (unique, tablename, f.column, tablename, f.column))
  645
+        table_output.append(" ".join(field_output))
  646
+    full_statement = ["CREATE TABLE %s (" % tablename]
  647
+    for i, line in enumerate(table_output):
  648
+        full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
  649
+    full_statement.append(');')
  650
+    curs = db.db.cursor()
  651
+    curs.execute("\n".join(full_statement))
  652
+    for statement in index_output:
  653
+        curs.execute(statement)
  654
+    db.db.commit()
  655
+createcachetable.args = "[tablename]"
10  docs/cache.txt
@@ -29,10 +29,20 @@ Examples:
29 29
     memcached://127.0.0.1:11211/    A memcached backend; the server is running
30 30
                                     on localhost port 11211.
31 31
 
  32
+    db://tablename/                 A database backend in a table named 
  33
+                                    "tablename". This table should be created
  34
+                                    with "django-admin createcachetable".
  35
+
  36
+    file:///var/tmp/django_cache/   A file-based cache stored in the directory
  37
+                                    /var/tmp/django_cache/.
  38
+
32 39
     simple:///                      A simple single-process memory cache; you
33 40
                                     probably don't want to use this except for
34 41
                                     testing. Note that this cache backend is
35 42
                                     NOT threadsafe!
  43
+                                        
  44
+    locmem:///                      A more sophisticaed local memory cache;
  45
+                                    this is multi-process- and thread-safe.
36 46
     ==============================  ===========================================
37 47
 
38 48
 All caches may take arguments -- they're given in query-string style.  Valid
8  docs/django-admin.txt
@@ -40,6 +40,14 @@ your admin's index page. See `Tutorial 2`_ for more information.
40 40
 
41 41
 .. _Tutorial 2: http://www.djangoproject.com/documentation/tutorial2/
42 42
 
  43
+createcachetable [tablename]
  44
+----------------------------
  45
+
  46
+Creates a cache table named ``tablename`` for use with the database cache
  47
+backend.  See the `cache documentation`_ for more information.
  48
+
  49
+.. _cache documentation: http://www.djangoproject.com/documentation/cache/
  50
+
43 51
 createsuperuser
44 52
 ---------------
45 53
 

0 notes on commit 0fa1aa8

Please sign in to comment.
Something went wrong with that request. Please try again.