Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #13946 -- Modified the database cache backend to use the databa…

…se router to determine availability of the cache table. Thanks to tiemonster for the report.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@13473 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit fc374976e51881d47203807e9843b4b2b3a0674b 1 parent 1c8547b
Russell Keith-Magee authored August 05, 2010
99  django/core/cache/backends/db.py
... ...
@@ -1,7 +1,7 @@
1 1
 "Database cache backend."
2 2
 
3 3
 from django.core.cache.backends.base import BaseCache
4  
-from django.db import connection, transaction, DatabaseError
  4
+from django.db import connections, router, transaction, DatabaseError
5 5
 import base64, time
6 6
 from datetime import datetime
7 7
 try:
@@ -9,10 +9,31 @@
9 9
 except ImportError:
10 10
     import pickle
11 11
 
  12
+class Options(object):
  13
+    """A class that will quack like a Django model _meta class.
  14
+
  15
+    This allows cache operations to be controlled by the router
  16
+    """
  17
+    def __init__(self, table):
  18
+        self.db_table = table
  19
+        self.app_label = 'django_cache'
  20
+        self.module_name = 'cacheentry'
  21
+        self.verbose_name = 'cache entry'
  22
+        self.verbose_name_plural = 'cache entries'
  23
+        self.object_name =  'CacheEntry'
  24
+        self.abstract = False
  25
+        self.managed = True
  26
+        self.proxy = False
  27
+
12 28
 class CacheClass(BaseCache):
13 29
     def __init__(self, table, params):
14 30
         BaseCache.__init__(self, params)
15  
-        self._table = connection.ops.quote_name(table)
  31
+        self._table = table
  32
+
  33
+        class CacheEntry(object):
  34
+            _meta = Options(table)
  35
+        self.cache_model_class = CacheEntry
  36
+
16 37
         max_entries = params.get('max_entries', 300)
17 38
         try:
18 39
             self._max_entries = int(max_entries)
@@ -25,17 +46,22 @@ def __init__(self, table, params):
25 46
             self._cull_frequency = 3
26 47
 
27 48
     def get(self, key, default=None):
28  
-        cursor = connection.cursor()
29  
-        cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % self._table, [key])
  49
+        db = router.db_for_read(self.cache_model_class)
  50
+        table = connections[db].ops.quote_name(self._table)
  51
+        cursor = connections[db].cursor()
  52
+
  53
+        cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % table, [key])
30 54
         row = cursor.fetchone()
31 55
         if row is None:
32 56
             return default
33 57
         now = datetime.now()
34 58
         if row[2] < now:
35  
-            cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key])
36  
-            transaction.commit_unless_managed()
  59
+            db = router.db_for_write(self.cache_model_class)
  60
+            cursor = connections[db].cursor()
  61
+            cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key])
  62
+            transaction.commit_unless_managed(using=db)
37 63
             return default
38  
-        value = connection.ops.process_clob(row[1])
  64
+        value = connections[db].ops.process_clob(row[1])
39 65
         return pickle.loads(base64.decodestring(value))
40 66
 
41 67
     def set(self, key, value, timeout=None):
@@ -47,56 +73,67 @@ def add(self, key, value, timeout=None):
47 73
     def _base_set(self, mode, key, value, timeout=None):
48 74
         if timeout is None:
49 75
             timeout = self.default_timeout
50  
-        cursor = connection.cursor()
51  
-        cursor.execute("SELECT COUNT(*) FROM %s" % self._table)
  76
+        db = router.db_for_write(self.cache_model_class)
  77
+        table = connections[db].ops.quote_name(self._table)
  78
+        cursor = connections[db].cursor()
  79
+
  80
+        cursor.execute("SELECT COUNT(*) FROM %s" % table)
52 81
         num = cursor.fetchone()[0]
53 82
         now = datetime.now().replace(microsecond=0)
54 83
         exp = datetime.fromtimestamp(time.time() + timeout).replace(microsecond=0)
55 84
         if num > self._max_entries:
56  
-            self._cull(cursor, now)
  85
+            self._cull(db, cursor, now)
57 86
         encoded = base64.encodestring(pickle.dumps(value, 2)).strip()
58  
-        cursor.execute("SELECT cache_key, expires FROM %s WHERE cache_key = %%s" % self._table, [key])
  87
+        cursor.execute("SELECT cache_key, expires FROM %s WHERE cache_key = %%s" % table, [key])
59 88
         try:
60 89
             result = cursor.fetchone()
61 90
             if result and (mode == 'set' or
62 91
                     (mode == 'add' and result[1] < now)):
63  
-                cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE cache_key = %%s" % self._table,
64  
-                               [encoded, connection.ops.value_to_db_datetime(exp), key])
  92
+                cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE cache_key = %%s" % table,
  93
+                               [encoded, connections[db].ops.value_to_db_datetime(exp), key])
65 94
             else:
66  
-                cursor.execute("INSERT INTO %s (cache_key, value, expires) VALUES (%%s, %%s, %%s)" % self._table,
67  
-                               [key, encoded, connection.ops.value_to_db_datetime(exp)])
  95
+                cursor.execute("INSERT INTO %s (cache_key, value, expires) VALUES (%%s, %%s, %%s)" % table,
  96
+                               [key, encoded, connections[db].ops.value_to_db_datetime(exp)])
68 97
         except DatabaseError:
69 98
             # To be threadsafe, updates/inserts are allowed to fail silently
70  
-            transaction.rollback_unless_managed()
  99
+            transaction.rollback_unless_managed(using=db)
71 100
             return False
72 101
         else:
73  
-            transaction.commit_unless_managed()
  102
+            transaction.commit_unless_managed(using=db)
74 103
             return True
75 104
 
76 105
     def delete(self, key):
77  
-        cursor = connection.cursor()
78  
-        cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key])
79  
-        transaction.commit_unless_managed()
  106
+        db = router.db_for_write(self.cache_model_class)
  107
+        table = connections[db].ops.quote_name(self._table)
  108
+        cursor = connections[db].cursor()
  109
+
  110
+        cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key])
  111
+        transaction.commit_unless_managed(using=db)
80 112
 
81 113
     def has_key(self, key):
  114
+        db = router.db_for_read(self.cache_model_class)
  115
+        table = connections[db].ops.quote_name(self._table)
  116
+        cursor = connections[db].cursor()
  117
+
82 118
         now = datetime.now().replace(microsecond=0)
83  
-        cursor = connection.cursor()
84  
-        cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s and expires > %%s" % self._table,
85  
-                       [key, connection.ops.value_to_db_datetime(now)])
  119
+        cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s and expires > %%s" % table,
  120
+                       [key, connections[db].ops.value_to_db_datetime(now)])
86 121
         return cursor.fetchone() is not None
87 122
 
88  
-    def _cull(self, cursor, now):
  123
+    def _cull(self, db, cursor, now):
89 124
         if self._cull_frequency == 0:
90 125
             self.clear()
91 126
         else:
92  
-            cursor.execute("DELETE FROM %s WHERE expires < %%s" % self._table,
93  
-                           [connection.ops.value_to_db_datetime(now)])
94  
-            cursor.execute("SELECT COUNT(*) FROM %s" % self._table)
  127
+            cursor.execute("DELETE FROM %s WHERE expires < %%s" % table,
  128
+                           [connections[db].ops.value_to_db_datetime(now)])
  129
+            cursor.execute("SELECT COUNT(*) FROM %s" % table)
95 130
             num = cursor.fetchone()[0]
96 131
             if num > self._max_entries:
97  
-                cursor.execute("SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" % self._table, [num / self._cull_frequency])
98  
-                cursor.execute("DELETE FROM %s WHERE cache_key < %%s" % self._table, [cursor.fetchone()[0]])
  132
+                cursor.execute("SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" % table, [num / self._cull_frequency])
  133
+                cursor.execute("DELETE FROM %s WHERE cache_key < %%s" % table, [cursor.fetchone()[0]])
99 134
 
100 135
     def clear(self):
101  
-        cursor = connection.cursor()
102  
-        cursor.execute('DELETE FROM %s' % self._table)
  136
+        db = router.db_for_write(self.cache_model_class)
  137
+        table = connections[db].ops.quote_name(self._table)
  138
+        cursor = connections[db].cursor()
  139
+        cursor.execute('DELETE FROM %s' % table)
8  django/db/backends/creation.py
@@ -353,9 +353,11 @@ def create_test_db(self, verbosity=1, autoclobber=False):
353 353
         call_command('syncdb', verbosity=verbosity, interactive=False, database=self.connection.alias)
354 354
 
355 355
         if settings.CACHE_BACKEND.startswith('db://'):
356  
-            from django.core.cache import parse_backend_uri
357  
-            _, cache_name, _ = parse_backend_uri(settings.CACHE_BACKEND)
358  
-            call_command('createcachetable', cache_name)
  356
+            from django.core.cache import parse_backend_uri, cache
  357
+            from django.db import router
  358
+            if router.allow_syncdb(self.connection.alias, cache.cache_model_class):
  359
+                _, cache_name, _ = parse_backend_uri(settings.CACHE_BACKEND)
  360
+                call_command('createcachetable', cache_name, database=self.connection.alias)
359 361
 
360 362
         # Get a cursor (even though we don't need one yet). This has
361 363
         # the side effect of initializing the test database.
43  docs/topics/cache.txt
@@ -136,6 +136,49 @@ settings file. You can't use a different database backend for your cache table.
136 136
 
137 137
 Database caching works best if you've got a fast, well-indexed database server.
138 138
 
  139
+Database caching and multiple databases
  140
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  141
+
  142
+If you use database caching with multiple databases, you'll also need
  143
+to set up routing instructions for your database cache table. For the
  144
+purposes of routing, the database cache table appears as a model named
  145
+``CacheEntry``, in an application named ``django_cache``. This model
  146
+won't appear in the models cache, but the model details can be used
  147
+for routing purposes.
  148
+
  149
+For example, the following router would direct all cache read
  150
+operations to ``cache_slave``, and all write operations to
  151
+``cache_master``. The cache table will only be synchronized onto
  152
+``cache_master``::
  153
+
  154
+    class CacheRouter(object):
  155
+        """A router to control all database cache operations"""
  156
+
  157
+        def db_for_read(self, model, **hints):
  158
+            "All cache read operations go to the slave"
  159
+            if model._meta.app_label in ('django_cache',):
  160
+                return 'cache_slave'
  161
+            return None
  162
+
  163
+        def db_for_write(self, model, **hints):
  164
+            "All cache write operations go to master"
  165
+            if model._meta.app_label in ('django_cache',):
  166
+                return 'cache_master'
  167
+            return None
  168
+
  169
+        def allow_syncdb(self, db, model):
  170
+            "Only synchronize the cache model on master"
  171
+            if model._meta.app_label in ('django_cache',):
  172
+                return db == 'cache_master'
  173
+            return None
  174
+
  175
+If you don't specify routing directions for the database cache model,
  176
+the cache backend will use the ``default`` database.
  177
+
  178
+Of course, if you don't use the database cache backend, you don't need
  179
+to worry about providing routing instructions for the database cache
  180
+model.
  181
+
139 182
 Filesystem caching
140 183
 ------------------
141 184
 

0 notes on commit fc37497

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