From 97d1f60d7729f37a516514e0d80c716f45e53b41 Mon Sep 17 00:00:00 2001 From: Jason Pellerin Date: Tue, 11 Jul 2006 20:37:07 +0000 Subject: [PATCH] [multi-db] Refactored connection handling to correct bugs in original design relating to thread and request isolation. Changed name of optional named databases attribute in settings to settings.OTHER_DATABASES. git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@3334 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/__init__.py | 193 +++++++++++++++++++++++++++++++++--------- 1 file changed, 154 insertions(+), 39 deletions(-) diff --git a/django/db/__init__.py b/django/db/__init__.py index ac27248824..c208134430 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -3,8 +3,18 @@ from django.core import signals from django.core.exceptions import ImproperlyConfigured from django.dispatch import dispatcher +try: + # Only exists in Python 2.4+ + from threading import local +except ImportError: + # Import copy of _thread_local.py from Python 2.4 + from django.utils._threading_local import local + __all__ = ('backend', 'connection', 'DatabaseError') +# singleton to represent the default connection in connections +_default = object() + if not settings.DATABASE_ENGINE: settings.DATABASE_ENGINE = 'dummy' @@ -13,22 +23,7 @@ def connect(settings): ConnectionInfo on succes, raises ImproperlyConfigured if the settings don't specify a valid database connection. """ - try: - backend = __import__('django.db.backends.%s.base' % settings.DATABASE_ENGINE, '', '', ['']) - except ImportError, e: - # The database backend wasn't found. Display a helpful error message - # listing all possible database backends. - import os - backend_dir = os.path.join(__path__[0], 'backends') - available_backends = [f for f in os.listdir(backend_dir) if not f.startswith('_') and not f.startswith('.') and not f.endswith('.py') and not f.endswith('.pyc')] - available_backends.sort() - if settings.DATABASE_ENGINE not in available_backends: - raise ImproperlyConfigured, "%r isn't an available database backend. vailable options are: %s" % \ - (settings.DATABASE_ENGINE, ", ".join(map(repr, available_backends))) - else: - raise # If there's some other error, this must be an error in Django itself. - - info = ConnectionInfo(backend, settings) + info = ConnectionInfo(settings) # Register an event that closes the database connection # when a Django request is finished. @@ -47,11 +42,13 @@ class ConnectionInfo(object): creation, introspection, and shell modules, closing the connection, and resetting the connection's query log. """ - def __init__(self, backend, settings): - self.backend = backend + def __init__(self, settings=None): + if settings is None: + from django.conf import settings self.settings = settings - self.connection = backend.DatabaseWrapper(settings) - self.DatabaseError = backend.DatabaseError + self.backend = self.load_backend() + self.connection = self.backend.DatabaseWrapper(settings) + self.DatabaseError = self.backend.DatabaseError def __repr__(self): return "Connection: %r (ENGINE=%s NAME=%s)" \ @@ -71,6 +68,33 @@ class ConnectionInfo(object): return __import__('django.db.backends.%s.creation' % self.settings.DATABASE_ENGINE, '', '', ['']) + def load_backend(self): + try: + backend = __import__('django.db.backends.%s.base' % + self.settings.DATABASE_ENGINE, '', '', ['']) + except ImportError, e: + # The database backend wasn't found. Display a helpful error + # message listing all possible database backends. + import os + backend_dir = os.path.join(__path__[0], 'backends') + available_backends = [f for f in os.listdir(backend_dir) + if not f.startswith('_') \ + and not f.startswith('.') \ + and not f.endswith('.py') \ + and not f.endswith('.pyc')] + available_backends.sort() + if settings.DATABASE_ENGINE not in available_backends: + raise ImproperlyConfigured, \ + "%r isn't an available database backend. "\ + "Available options are: %s" % \ + (settings.DATABASE_ENGINE, + ", ".join(map(repr, available_backends))) + else: + # If there's some other error, this must be an error + # in Django itself. + raise + return backend + def runshell(self): __import__('django.db.backends.%s.client' % self.settings.DATABASE_ENGINE, '', '', ['']).runshell() @@ -79,70 +103,161 @@ class ConnectionInfo(object): """Reset log of queries executed by connection""" self.connection.queries = [] - + class LazyConnectionManager(object): """Manages named connections lazily, instantiating them as they are requested. """ def __init__(self): - self._connections = {} + self.local = local() + self.local.connections = {} def __iter__(self): - return self._connections.keys() + return self.local.connections.keys() def __getattr__(self, attr): - # use __dict__ to avoid getattr() loop - return getattr(self.__dict__['_connections'], attr) + return getattr(self.local.connections, attr) def __getitem__(self, k): try: - return self.__dict__['_connections'][k] + return self.local.connections[k] except KeyError: return self.connect(k) def __setitem__(self, k, v): - self.__dict__['_connections'][k] = v + self.local.connections[k] = v def connect(self, name): """Return the connection with this name in - settings.DATABASES. Creates the connection if it doesn't yet - exist. Reconnects if it does. + settings.OTHER_DATABASES. Creates the connection if it doesn't yet + exist. Reconnects if it does. If the name requested is the default + connection (a singleton defined in django.db), then the default + connection is returned. """ - if name in self._connections: - self._connections[name].close() + cnx = self.local.connections + if name in cnx: + cnx[name].close() + if name is _default: + # get the default connection from connection_info + if connection_info.local.db is None: + connection_info.init_connection() + cnx[name] = connection_info.local.db + return cnx[name] try: - info = settings.DATABASES[name] + info = settings.OTHER_DATABASES[name] except KeyError: raise ImproperlyConfigured, \ "No database connection '%s' has been configured" % name except AttributeError: raise ImproperlyConfigured, \ - "No DATABASES in settings." + "No OTHER_DATABASES in settings." - # In settings it's a dict, but connect() needs an object + # In settings it's a dict, but connect() needs an object: # pass global settings so that the default connection settings - # can be defaults for the named connections + # can be defaults for the named connections. database = UserSettingsHolder(settings) for k, v in info.items(): setattr(database, k, v) - self._connections[name] = connect(database) - return self._connections[name] + cnx[name] = connect(database) + return cnx[name] + + def reset(self): + self.local.connections = {} +class _proxy: + """A lazy-initializing proxy. The proxied object is not + initialized until the first attempt to access it. + """ + + def __init__(self, init_obj): + self.__dict__['_obj'] = None + self.__dict__['_init_obj'] = init_obj + + def __getattr__(self, attr): + if self.__dict__['_obj'] is None: + self.__dict__['_obj'] = self.__dict__['_init_obj']() + return getattr(self.__dict__['_obj'], attr) + + def __setattr__(self, attr, val): + if self.__dict__['_obj'] is None: + self.__dict__['_obj'] = self.__dict__['_init_obj']() + setattr(self.__dict__['_obj'], attr, val) + + +class DefaultConnectionInfoProxy(object): + """Holder for proxy objects that will connect to the current + default connection when used. Mimics the interface of a ConnectionInfo. + """ + def __init__(self): + self.local = local() + self.local.db = None + self.connection = _proxy(self.get_connection) + self.DatabaseError = _proxy(self.get_database_error) + self.backend = _proxy(self.get_backend) + self.get_introspection_module = _proxy(self.get_introspection_module) + self.get_creation_module = _proxy(self.get_creation_module) + self.runshell = _proxy(self.get_runshell) + + def init_connection(self): + from django.conf import settings + self.local.db = connect(settings) + + def get_backend(self): + if self.local.db is None: + self.init_connection() + return self.local.db.backend + + def get_connection(self): + if self.local.db is None: + self.init_connection() + return self.local.db.connection + + def get_database_error(self): + if self.local.db is None: + self.init_connection() + return self.local.db.DatabaseError + + def get_introspection_module(self): + if self.local.db is None: + self.init_connection() + return self.local.db.get_introspection_module + + def get_creation_module(self): + if self.local.db is None: + self.init_connection() + return self.local.db.get_creation_module + + def get_runshell(self): + if self.local.db is None: + self.init_connection() + return self.local.db.runshell + + def close(self): + self.local.db = None + + # Backwards compatibility: establish the default connection and set the # default connection properties at module level -connection_info = connect(settings) +connection_info = DefaultConnectionInfoProxy() (connection, DatabaseError, backend, get_introspection_module, get_creation_module, runshell) = (connection_info.connection, connection_info.DatabaseError, connection_info.backend, - connection_info.get_introspection_module, + connection_info.get_introspection_module, connection_info.get_creation_module, connection_info.runshell) # Create a manager for named connections connections = LazyConnectionManager() +# Reset connections on request finish, to make sure each request can +# load the correct connections for its settings +dispatcher.connect(connections.reset, signal=signals.request_finished) + +# Clear the default connection on request finish also +dispatcher.connect(connection_info.close, signal=signals.request_finished) + # Register an event that rolls back all connections # when a Django request has an exception. def _rollback_on_exception():