Chapter 11 · Database backends
11.1 Connections by alias: DATABASES and ConnectionHandler
django, the one ConnectionHandler of a process, turns an alias of the setting DATABASES into a connection that the calling thread may use. It fills in the setting’s defaults once, makes a connection the first time a thread asks for an alias, importing its backend where that has not been done, and keeps it in storage of that thread’s own; django is a proxy for the connection "default".
Code that sends a statement starts from an alias, a key of DATABASES, and needs the connection for it: an instance of the backend’s DatabaseWrapper, such as SQLite’s, that it can use at once. Three things stand in between. The settings a project wrote may leave out most of their keys, or be empty; the backend is a dotted path still to be imported; and a connection carries the driver’s connection and the state of a transaction, which Django does not let a second thread take a cursor on, commit or roll back unless told that it may.
ConnectionHandler deals with all three behind one subscript, connections["default"]. Making a connection opens nothing: the driver’s connection is opened the first time something needs it (Opening and closing a connection).
django.db.connections keeps one set of settings for the process and, in each thread, a connection for each alias that thread has asked for, made the first time it asked. In a thread that runs an event loop the storage is divided again, by asyncio context.The handler, and its first use
django.db makes the handler as the package is imported, with no settings, and the handler reads nothing until BaseConnectionHandler, a cached property, is first read. That first read calls ConnectionHandler, which reads DATABASES where the handler was given no settings and adds the defaults; the result is kept for the life of the handler.
The first use in a process was recorded from a running Django, in an interpreter of its own, with the example project’s two SQLite databases, "default" and "archive". A line at the left margin is a question with its answer after ->, or a line of Python with the calls it set going nested under it, only the calls the passage is about written down and -> for what a call returned; a call is written by the class that declares its method, or by the object’s class with the declaring class after it in parentheses:
'settings' in vars(connections) -> False
'django.db.backends.sqlite3.base' in sys.modules -> False
django.setup()
import_module('harbour.fleet.models')
Options.contribute_to_class(cls=Named, name='_meta')
BaseConnectionHandler.__getitem__('default')
BaseConnectionHandler.settings
ConnectionHandler.configure_settings
BaseConnectionHandler.configure_settings
-> a dict of 2: 'default', 'archive', each with its defaults filled in
ConnectionHandler.create_connection('default')
load_backend('django.db.backends.sqlite3')
import_module('django.db.backends.sqlite3.base')
-> the module django.db.backends.sqlite3.base
DatabaseWrapper.__init__(settings_dict=connections.settings['default'], alias='default') (BaseDatabaseWrapper.__init__)
...
-> the connection "default"
What first asked was not a query. Options, setting up the first model class, the fleet’s abstract Named, asks the default connection’s operations object for the longest name the database allows, to cut the default table name to it (The options: what a model keeps as _meta). That one subscript configured the settings of every alias, imported SQLite’s backend through load_backend, and made the connection, whose BaseDatabaseWrapper made the helper objects (The connection object: BaseDatabaseWrapper and what a backend provides). Afterwards the connection is in this thread’s storage, and the dictionary it holds as BaseDatabaseWrapper is the handler’s own for the alias, not a copy:
'settings' in vars(connections) -> True
connections.all(initialized_only=True) -> [the connection "default"]
'django.db.backends.sqlite3.base' in sys.modules -> True
conn = connections['default']
connections['default'] is conn -> True
conn.settings_dict is connections.settings['default'] -> True
conn.connection -> None
The defaults, and the alias "default"
ConnectionHandler settles two things: that there is an alias "default", and that every alias has a value for each of the keys below. For an alias given an engine and a name and nothing else, a key to a line:
given = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'harbour.sqlite3'}}
handler = ConnectionHandler(given)
handler.settings is given -> True
settings['default']['ENGINE']: 'django.db.backends.sqlite3'
settings['default']['NAME']: 'harbour.sqlite3'
settings['default']['ATOMIC_REQUESTS']: False
settings['default']['AUTOCOMMIT']: True
settings['default']['CONN_MAX_AGE']: 0
settings['default']['CONN_HEALTH_CHECKS']: False
settings['default']['OPTIONS']: {}
settings['default']['TIME_ZONE']: None
settings['default']['USER']: ''
settings['default']['PASSWORD']: ''
settings['default']['HOST']: ''
settings['default']['PORT']: ''
settings['default']['TEST']: {'CHARSET': None, 'COLLATION': None, 'MIGRATE': True, 'MIRROR': None, 'NAME': None}
Each default is written with dict.setdefault, so a key the project gave keeps its value, except an "ENGINE" that names no backend (below). The defaults go into the setting’s own dictionaries: the recorded handler kept the very dictionary it was given, and the handler of django.db fills in the object that settings holds. Each key is read where its subject is told: "ENGINE" by the handler, to load the backend; "NAME", "USER", "PASSWORD", "HOST", "PORT" and "OPTIONS" by each backend that uses them (SQLite, PostgreSQL, MySQL and MariaDB, Oracle); "AUTOCOMMIT", "CONN_MAX_AGE" and "CONN_HEALTH_CHECKS" as a connection is opened and closed (Opening and closing a connection); "ATOMIC_REQUESTS" around each view (Transactions: autocommit, atomic and savepoints); "TIME_ZONE" where values cross (Values in and out: adapters, converters and time zones); and "TEST" when the test databases are made (Test databases: BaseDatabaseCreation).
The alias "default" is settled first. An empty setting, which is also what DATABASES is in Django’s global settings, is given a "default" whose engine is "django. A setting with aliases, none of them "default", is refused. A "default" given as an empty dictionary is given the dummy engine, and so, in the loop that follows, is any alias whose "ENGINE" is missing, empty, None or the bare "django:
empty = ConnectionHandler({})
empty.settings['default']['ENGINE'] -> 'django.db.backends.dummy'
...
ConnectionHandler({'other': {'ENGINE': 'django.db.backends.sqlite3'}}).settings -> raises ImproperlyConfigured: You must define a 'default' database.
ConnectionHandler({'default': {}}).settings['default']['ENGINE'] -> 'django.db.backends.dummy'
So the alias may be a placeholder, but it must be there. "default", which Django names DEFAULT_DB_ALIAS, is the alias it falls back on wherever no other has been chosen: the router’s answer when no router answers and no instance among its hints has a database (Routing between databases: ConnectionRouter), the alias of django, and the one the functions of django use when they are given none. Django’s guide to several databases puts it from the user’s side: an entry for "default" is required, and its dictionary may be left empty where routers send every query elsewhere.
create_connection, and importing the backend
ConnectionHandler hands the alias’s "ENGINE" to load_backend and calls the DatabaseWrapper of the module that comes back with the alias’s settings and the alias. The class is looked up by its name, so a backend is any package whose base module defines one (The connection object: BaseDatabaseWrapper and what a backend provides).
load_backend imports the backend’s module base, which Python then keeps in sys.modules, so the import is done once in a process. Where the import raises ImportError, the name decides what follows. A name that is not a built-in backend’s, a package under django/ other than "base" and "dummy", is refused with ImproperlyConfigured, which lists the built-ins and is raised from the ImportError, so that the cause stands above it in the traceback; a built-in backend’s ImportError is raised again, the comment taking it for an error in Django:
load_backend('harbour.nosuch') -> raises ImproperlyConfigured: 'harbour.nosuch' isn't an available database backend or couldn't be imported. Check the above exception. To use one of the built-in backends, use 'django.db.backends.XXX', where XXX is one of:\n 'mysql', 'oracle', 'postgresql', 'sqlite3'
PostgreSQL’s, MySQL’s and Oracle’s backends never reach that branch when their driver is not installed: each base module catches its driver’s ImportError and raises an ImproperlyConfigured of its own.
One connection for each alias in each thread
BaseConnectionHandler returns what it finds in the storage under the alias. Finding nothing, it refuses an alias the settings lack with the class’s BaseConnectionHandler, ConnectionDoesNotExist, and otherwise calls ConnectionHandler and stores what it returns.
connections['nosuch'] -> raises ConnectionDoesNotExist: The connection 'nosuch' doesn't exist.
The storage is asgiref’s Local, made with BaseConnectionHandler, which ConnectionHandler sets to True. With that flag, in a thread that runs no event loop, Local behaves exactly as a threading.local: what one thread sets, only that thread sees. Here the main thread holds "default" and a second thread asks for the same alias:
main_connection = connections['default']
main_connection.connection is None -> True
# on a second thread, which the main thread waits for:
connections.all(initialized_only=True) -> []
connections['default'] is main_connection -> False
connections.all(initialized_only=True) -> [the connection "default"]
Each thread is given its own connection; BaseConnectionHandler with initialized_only=True lists what the asking thread has made. A connection refuses another thread a cursor, a commit or a rollback unless it has been allowed to be shared (Opening and closing a connection). Only the connection objects are kept apart. Every thread’s connection for an alias holds the one dictionary of settings, so a change made to it through one is seen by all.
BaseConnectionHandler puts a connection into this thread’s storage without a check.
In a thread where an event loop is running, Local keeps within the thread’s store a second one that behaves as a context variable. A connection a coroutine makes is seen by what it awaits and by tasks it starts afterwards, not by its thread outside the loop: a coroutine asking for "default" is given a connection other than the one its thread holds in synchronous code, and two tasks that each ask first are given two, which the coroutine that started them does not see. Each task here returns the connection it is given:
asyncio.run(in_a_new_loop()) # the script's coroutine, whose lines stand under this one; nothing in it has asked for a connection before
first, second = await asyncio.gather(asks_first(), asks_first())
first is second -> False
first is main_connection -> False
connections['default'] is main_connection -> False
connections['default'] is first -> False
third, fourth = await asyncio.gather(asks_first(), asks_first())
third is fourth is connections['default'] -> True
threading.current_thread() is threading.main_thread() -> True
In a thread whose event loop is running, any connection refuses to open or to give a cursor, unless the environment variable "DJANGO_ALLOW_ASYNC_UNSAFE" is set (The asynchronous methods of a queryset). The comment on ConnectionHandler gives the reasoning: backends are to guard their code against asynchronous contexts with async_unsafe, the storage gives such contexts separate connections in case one is needed all the same, and since nothing cleans up after them, using such a connection is not allowed where it can be helped. The work goes to another thread through sync_to_async, which here runs every call on one thread kept for the purpose; that thread, running no loop, keeps connections of its own from one call to the next:
asyncio.run(in_the_loop()) # the script's coroutine, whose lines stand under this one
threading.current_thread() is threading.main_thread() -> True
connections['default'].cursor() -> raises SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
Ship.objects.count() -> raises SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
await sync_to_async(threading.current_thread)() is threading.main_thread() -> False
await sync_to_async(connections.__getitem__)('default') is main_connection -> False
await sync_to_async(threading.get_ident)() == await sync_to_async(threading.get_ident)() -> True
await sync_to_async(Ship.objects.count)() -> 2
SQL SELECT COUNT(*) AS "__count" FROM "fleet_ship"
django.db.connection : a proxy for "default"
django is a ConnectionProxy of the handler and DEFAULT_DB_ALIAS, made as django.db is imported, with a comment that it is kept for backwards compatibility and that connections['default'] is to be preferred. The proxy holds the handler and the alias and nothing else. Each attribute read that the proxy itself lacks, each one set or deleted on it, and its in and its ==, go to connections["default"], looked up again at that moment, so the one name stands in each thread for that thread’s connection.
type(connection) -> django.utils.connection.ConnectionProxy
connection == connections['default'] -> True
connection is connections['default'] -> False
connection.alias -> 'default'
The dummy backend
django is the backend of an alias that names no engine. Two functions of the module are put in place of the base classes’ methods, as class attributes: complain, which raises ImproperlyConfigured, and ignore, which does nothing. The comment on the dummy’s DatabaseWrapper gives the line between them: whatever tries actually to do something complains, and whatever tries to roll back or undo something is ignored. empty is the handler given an empty setting:
type(empty['default']) -> django.db.backends.dummy.base.DatabaseWrapper
empty['default'].cursor() -> raises ImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.
empty['default'].close() -> None
So the connection is made like any other; whatever would open it through BaseDatabaseWrapper, or take a cursor, stops with a message that names the missing value, and BaseDatabaseWrapper returns at once, there being no driver’s connection. BaseDatabaseWrapper returns quietly where BaseDatabaseWrapper complains, and BaseDatabaseWrapper returns before making a savepoint, DummyDatabaseFeatures being false:
empty['default'].rollback() -> None
empty['default'].features.uses_savepoints -> False
empty['default'].savepoint() -> None
empty['default'].commit() -> raises ImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.
A model class can still be set up, the base class’s BaseDatabaseOperations answering None, no limit.
all, close_all and the receivers of django.db
BaseConnectionHandler returns self[alias] for each alias of the settings, which makes every connection this thread has not made, unless initialized_only=True is given: then only what this thread’s storage holds is returned. BaseConnectionHandler closes each connection this thread has made. BaseCommand calls it after a command run from the command line; the server a live server’s thread runs, which serves each client’s connection on a thread of its own, calls it when it has finished with that connection, and the live server’s thread calls it as it ends.
django/ ends by connecting two functions to the signals a handler sends at a request’s start and end (The two entry points). reset_queries, on request_started, empties the query log, BaseDatabaseWrapper, of each connection this thread has made (Cursors: executing a statement, the query log and execute wrappers). close_old_connections, on request_started and request_finished both, calls BaseDatabaseWrapper on each (Opening and closing a connection). Both pass initialized_only=True, so neither makes a connection. Here the two signals are sent by hand, as a handler sends them:
signals.request_started.send(sender=WSGIHandler, environ={})
Signal.send (request_started, sender=WSGIHandler)
reset_queries
close_old_connections
BaseDatabaseWrapper.close_if_unusable_or_obsolete
...
signals.request_finished.send(sender=WSGIHandler)
Signal.send (request_finished, sender=WSGIHandler)
close_old_connections
BaseDatabaseWrapper.close_if_unusable_or_obsolete
Under ASGI both signals reach the receivers through sync_to_async, on the request’s own thread, where its connections were made (Sync and async in one chain).