ZetCode

Django database configuration

last modified November 13, 2026 Python 3.15

In this article we configure the database of a Django project. We start with the default SQLite database and its options, and then connect the same project to PostgreSQL and MariaDB running in containers. We look at persistent connections, health checks, and connection pooling, and finally split a project into two databases with a database router. The examples use Django 6.1 and Python 3.15, PostgreSQL 18, and MariaDB 12.

The DATABASES setting

Django reads the database connections from the DATABASES setting, a dictionary that maps an alias to a configuration. Every project needs the alias default; other aliases are used when a project talks to several databases. A configuration has an ENGINE, which is the Python path of the backend, a NAME, the connection parameters such as USER, PASSWORD, HOST and PORT, and optional keys such as OPTIONS, CONN_MAX_AGE and TEST. Django includes backends for SQLite, PostgreSQL, MySQL (which also serves MariaDB), and Oracle. Except for SQLite, whose driver is part of Python, a backend needs a driver package.

Databases other than these are available through third-party backends, which we do not cover.

The example project

The project is created with uv; the steps are explained in Django introduction. The commands below create a project directory, install Django, generate the project with the configuration package config, start the app shop, and create the folder demos for demo scripts.

$ mkdir database-configuration
$ cd database-configuration
$ uv init --bare
$ uv add django
$ uv run django-admin startproject config .
$ uv run python manage.py startapp shop
$ mkdir demos

The trailing dot in startproject config . creates manage.py and the config package in the current directory instead of one level deeper. A new app is ignored by Django until it is listed in INSTALLED_APPS, so we add the app to the list in config/settings.py; the other entries are generated by startproject.

config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'shop',
]

The listings in this article are of two kinds. A listing with a solid blue border and the label Project file is a file of the application: models, views, templates, tests, and settings. A listing with a dashed amber border and the label Demo script is a small helper in the demos folder; it sets up Django, runs a few lines of code, and prints the results, so that we can watch the behaviour. Demo scripts are not part of the application; we run one from the project directory with uv run python -m demos.name.

The package config holds the settings and the app shop has one model. To compare the backends, we keep the settings of each in a separate module, such as config/settings_postgres.py, that imports the base settings and replaces DATABASES. We select a module with the DJANGO_SETTINGS_MODULE environment variable or the --settings option; in a real project, a single settings file is enough.

shop/models.py
from django.db import models


class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=8, decimal_places=2)

    def __str__(self):
        return self.name

A product has a name and a price. We create the migration with makemigrations; the migration file does not depend on the database, and the same file is applied to every backend (see Django migrations).

$ uv run python manage.py makemigrations shop
Migrations for 'shop':
  shop/migrations/0001_initial.py
    + Create model Product

The default SQLite database

The settings file generated by startproject configures SQLite. The database is a single file in the project directory, and the value of NAME is its path. SQLite needs no server and no setup, which makes it ideal for learning, development, tests, and small sites with few concurrent writers.

config/settings.py (DATABASES)
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

The migrate command creates the file and the tables. The script demos/info.py, listed next, prints the vendor of the backend, the name of the database, and the version of the server, using the object django.db.connection, which represents the connection of the default database.

demos/info.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.db import connection

settings = connection.settings_dict
print('vendor:', connection.vendor)
print('engine:', settings['ENGINE'])
print('name:  ', settings['NAME'])

sql = 'SELECT sqlite_version()' if connection.vendor == 'sqlite' else 'SELECT version()'
with connection.cursor() as cursor:
    cursor.execute(sql)
    print('server:', cursor.fetchone()[0])

The vendor attribute is a short name of the backend (sqlite, postgresql, mysql, oracle), which is useful for code that must behave differently for each database. The script chooses the SQL that returns the version, because SQLite has sqlite_version() and the other databases have version().

$ uv run python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, shop
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying shop.0001_initial... OK
$ uv run python -m demos.info
vendor: sqlite
engine: django.db.backends.sqlite3
name:   ./db.sqlite3
server: 3.53.1

SQLite options

SQLite is configured with the keys of the OPTIONS dictionary. Since Django 5.1, the init_command option runs SQL statements for each new connection, and the transaction_mode option selects the type of transaction that atomic() begins; the timeout option is the number of seconds that a connection waits for a lock held by another connection. A write-heavy site can improve concurrency with the write-ahead log mode and relaxed synchronization, and a transaction mode of IMMEDIATE makes a transaction take the write lock at the beginning, avoiding the error "database is locked" in the middle of a transaction.

config/settings_sqlite.py
from .settings import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
        'OPTIONS': {
            'timeout': 20,
            'transaction_mode': 'IMMEDIATE',
            'init_command': 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;',
        },
    }
}

The script reads the options back and asks SQLite for the three values with PRAGMA statements.

demos/sqlite.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.db import connection

print('OPTIONS:', connection.settings_dict['OPTIONS'])
with connection.cursor() as cursor:
    for pragma in ('journal_mode', 'synchronous', 'busy_timeout'):
        cursor.execute(f'PRAGMA {pragma}')
        print(pragma, '=', cursor.fetchone()[0])

We run the script twice: with the default settings, and with the settings module that sets the options. By default, the journal mode is delete, synchronous is 2 (full), and the busy timeout is 5000 milliseconds, which is the default 5 seconds of the timeout option. The second run shows the write-ahead log mode, synchronous 1 (normal), and the timeout of 20 seconds.

$ uv run python -m demos.sqlite
OPTIONS: {}
journal_mode = delete
synchronous = 2
busy_timeout = 5000
$ DJANGO_SETTINGS_MODULE=config.settings_sqlite uv run python -m demos.sqlite
OPTIONS: {'timeout': 20, 'transaction_mode': 'IMMEDIATE', 'init_command': 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;'}
journal_mode = wal
synchronous = 1
busy_timeout = 20000

Running PostgreSQL and MariaDB in containers

For the next sections we need a PostgreSQL and a MariaDB server. The easiest way is to run them as containers. The commands create a database named shop and a user with the password secret in each of them; the ports are mapped to 5433 and 3307 so that they do not collide with servers installed on the machine. Any other installation of the databases will do as well.

$ docker run -d --rm --name djpg -e POSTGRES_USER=shop -e POSTGRES_PASSWORD=secret \
    -e POSTGRES_DB=shop -p 127.0.0.1:5433:5432 postgres:18
$ docker run -d --rm --name djmaria -e MARIADB_ROOT_PASSWORD=rootsecret -e MARIADB_USER=shop \
    -e MARIADB_PASSWORD=secret -e MARIADB_DATABASE=shop -p 127.0.0.1:3307:3306 mariadb:12
$ docker exec djmaria mariadb -uroot -prootsecret \
    -e "GRANT ALL ON \`test\_%\`.* TO 'shop'@'%'"

The third command lets the MariaDB user create the test databases, which we need at the end of the article. The containers are removed when they stop (--rm), and so is their data; this is what we want for experiments. The passwords are throwaway values; a real project must never keep real credentials in the settings that go to version control.

PostgreSQL

PostgreSQL is the database that most Django projects use in production. The backend needs the psycopg package. The extra binary installs a precompiled libpq, so no compiler or PostgreSQL headers are needed, and the extra pool installs the connection pool that we use later.

$ uv add "psycopg[binary,pool]"

Next comes the configuration. The values come from environment variables, with defaults for our local container, so that the same settings work on a developer machine and on a server where the variables are set by the deployment.

config/settings_postgres.py
import os

from .settings import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', 'shop'),
        'USER': os.environ.get('DB_USER', 'shop'),
        'PASSWORD': os.environ.get('DB_PASSWORD', 'secret'),
        'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
        'PORT': os.environ.get('DB_PORT', '5433'),
    }
}

The engine is django.db.backends.postgresql. The NAME is the database, and the database and the user must already exist; Django creates tables, not databases. The first run prints the information about the connection. Because the container contains an empty database, the migrate shop command applies only the migrations of our app and, to keep the output short, we do not run the full migrate. The last command lists the tables in PostgreSQL itself.

$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.info
vendor: postgresql
engine: django.db.backends.postgresql
name:   shop
server: PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
$ uv run python manage.py migrate shop --settings=config.settings_postgres
Operations to perform:
  Apply all migrations: shop
Running migrations:
  Applying shop.0001_initial... OK
$ docker exec djpg psql -U shop -c "\dt"
               List of tables
 Schema |       Name        | Type  | Owner 
--------+-------------------+-------+-------
 public | django_migrations | table | shop
 public | shop_product      | table | shop
(2 rows)

MariaDB and MySQL

The mysql backend works with MariaDB and MySQL, and the driver is mysqlclient. It is a C extension, which is built when it is installed, so it needs a compiler and the client library with the headers; on Debian-based systems they come in the libmariadb-dev package. If the build cannot find the library, set the MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS environment variables.

$ uv add mysqlclient

The configuration has the same shape as that for PostgreSQL. The charset option sets the connection character set; with utf8mb4 the full Unicode, including emoji, can be stored.

config/settings_mariadb.py
import os

from .settings import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': os.environ.get('DB_NAME', 'shop'),
        'USER': os.environ.get('DB_USER', 'shop'),
        'PASSWORD': os.environ.get('DB_PASSWORD', 'secret'),
        'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
        'PORT': os.environ.get('DB_PORT', '3307'),
        'OPTIONS': {'charset': 'utf8mb4'},
    }
}

The script demos/info.py prints the same information. Django reports the vendor as mysql for both servers; the version string shows that the server is MariaDB. We apply the migrations and list the tables with the mariadb client inside the container.

$ DJANGO_SETTINGS_MODULE=config.settings_mariadb uv run python -m demos.info
vendor: mysql
engine: django.db.backends.mysql
name:   shop
server: 12.3.3-MariaDB-ubu2404
$ uv run python manage.py migrate shop --settings=config.settings_mariadb
Operations to perform:
  Apply all migrations: shop
Running migrations:
  Applying shop.0001_initial... OK
$ docker exec djmaria mariadb -ushop -psecret shop -e "SHOW TABLES"
Tables_in_shop
django_migrations
shop_product

Persistent connections

By default, Django opens a database connection at the beginning of a request and closes it at the end, which is the setting CONN_MAX_AGE = 0. Opening a connection costs time, mainly with a remote server and TLS. With CONN_MAX_AGE set to a number of seconds, a worker thread keeps the connection and reuses it for the following requests until it is older than that value; None means that the connection lives forever. At the beginning and at the end of each request, Django calls close_old_connections(), which closes the connections that are too old or broken. Each thread has its own connection, so the number of open connections can reach the number of the worker threads of all the server processes, which must stay below the limit of the server (max_connections in PostgreSQL).

demos/conn.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.db import close_old_connections, connection


def backend_pid():
    with connection.cursor() as cursor:
        cursor.execute('SELECT pg_backend_pid()')
        return cursor.fetchone()[0]


for age in (0, 60):
    connection.close()
    connection.settings_dict['CONN_MAX_AGE'] = age
    first = backend_pid()
    close_old_connections()
    second = backend_pid()
    print(f'CONN_MAX_AGE={age}: same connection after the "request": {first == second}')

The script simulates two requests by calling close_old_connections() between two queries, as Django does when a request finishes. The function pg_backend_pid() returns the id of the server process that serves the connection, which tells us whether it is the same connection. With the age 0 the second query runs on a new connection; with the age 60 it reuses the first one.

$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.conn
CONN_MAX_AGE=0: same connection after the "request": False
CONN_MAX_AGE=60: same connection after the "request": True

Connection health checks

A persistent connection can be closed by the server, by a restart, a failover, or a firewall that drops idle connections. The next request that uses it fails with an OperationalError. The setting CONN_HEALTH_CHECKS = True makes Django check a reused connection before the first query of a request and open a new one if the check fails.

demos/health.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

import psycopg
from django.db import OperationalError, close_old_connections, connection


def backend_pid():
    with connection.cursor() as cursor:
        cursor.execute('SELECT pg_backend_pid()')
        return cursor.fetchone()[0]


def terminate(pid):
    s = connection.settings_dict
    with psycopg.connect(dbname=s['NAME'], user=s['USER'], password=s['PASSWORD'],
                         host=s['HOST'], port=s['PORT'], autocommit=True) as other:
        other.execute('SELECT pg_terminate_backend(%s)', [pid])


for checks in (False, True):
    connection.close()
    connection.settings_dict.update(CONN_MAX_AGE=60, CONN_HEALTH_CHECKS=checks)
    pid = backend_pid()
    terminate(pid)
    close_old_connections()
    try:
        print(f'CONN_HEALTH_CHECKS={checks}: new connection, query ok:', backend_pid() != pid)
    except OperationalError as error:
        print(f'CONN_HEALTH_CHECKS={checks}: {type(error).__name__}:', str(error).strip())

The script opens a persistent connection, terminates its server process from a second connection with pg_terminate_backend(), and then runs a query, once with the health checks off and once with them on. Without the check the query fails on the dead connection; with the check Django notices that the connection is unusable, opens a new one, and the query succeeds.

$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.health
CONN_HEALTH_CHECKS=False: OperationalError: terminating connection due to administrator command
CONN_HEALTH_CHECKS=True: new connection, query ok: True

Connection pooling

Since Django 5.1 the PostgreSQL backend can use a connection pool, which is available with psycopg 3 and the pool extra. The pool is enabled with the pool key of OPTIONS; True accepts the defaults, and a dictionary passes the arguments to the ConnectionPool of psycopg_pool. A connection that a thread closes returns to the pool and is reused by another thread, so the number of connections to the server is limited by max_size, no matter how many threads there are. A pool does not work with persistent connections: CONN_MAX_AGE must be left at 0, otherwise Django raises ImproperlyConfigured.

config/settings_pool.py
from .settings_postgres import *

DATABASES['default']['OPTIONS'] = {
    'pool': {'min_size': 2, 'max_size': 4, 'timeout': 10},
}

The pool keeps at least two connections and opens at most four; a thread that needs a connection when all four are in use waits up to ten seconds. The next script runs forty queries in eight threads, and each thread closes its connection after the query, as the worker threads of a server do at the end of a request.

demos/threads.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from concurrent.futures import ThreadPoolExecutor

from django.db import connection


def work(_):
    with connection.cursor() as cursor:
        cursor.execute('SELECT pg_backend_pid()')
        pid = cursor.fetchone()[0]
    connection.close()
    return pid


with ThreadPoolExecutor(max_workers=8) as executor:
    pids = list(executor.map(work, range(40)))

print('queries:', len(pids))
print('server connections used:', len(set(pids)))
print('pool:', connection.settings_dict['OPTIONS'].get('pool'))

We run the script with the plain PostgreSQL settings and with the pool. Without the pool every query opens a new connection and the server sees forty of them. With the pool, the same work goes through a few connections, never more than four; the exact number depends on the timing.

$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.threads
queries: 40
server connections used: 40
pool: None
$ DJANGO_SETTINGS_MODULE=config.settings_pool uv run python -m demos.threads
queries: 40
server connections used: 3
pool: {'min_size': 2, 'max_size': 4, 'timeout': 10}

Multiple databases

A project can use several databases. We add another alias to DATABASES; here the second database is named analytics and lives on the same PostgreSQL server. We create a second app, analytics, with a model PageView that records page visits; we want its table in the analytics database, and the rest of the project in shop. The first command creates the app, the second creates the database on the server with createdb.

$ uv run python manage.py startapp analytics
$ docker exec djpg createdb -U shop analytics

The model stores the visited path and the time of the visit; the field auto_now_add fills the time when the row is created.

analytics/models.py
from django.db import models


class PageView(models.Model):
    path = models.CharField(max_length=200)
    viewed_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.path

The settings module reuses the PostgreSQL connection parameters with a different name, installs the app, and registers a router.

config/settings_multi.py
from .settings_postgres import *

INSTALLED_APPS = INSTALLED_APPS + ['analytics']

DATABASES['analytics'] = {**DATABASES['default'], 'NAME': 'analytics'}

DATABASE_ROUTERS = ['config.routers.AnalyticsRouter']

A database router is a class that tells Django which database to use. The methods db_for_read() and db_for_write() return an alias for a model, or None to let the next router or the default decide. The method allow_migrate() decides whether the migrations of an app run on a given database, and allow_relation() whether a relation between two objects is permitted.

config/routers.py
class AnalyticsRouter:
    """Send the analytics app to the analytics database."""

    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'analytics':
            return 'analytics'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'analytics':
            return 'analytics'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == 'analytics':
            return db == 'analytics'
        return db == 'default'

Our router sends the models of the analytics app to the analytics database. It allows the migrations of that app only there, and everything else only on default. allow_relation() returns None, which means "no opinion": by default Django permits relations only between objects from the same database. Foreign keys between models in different databases are not supported, because a database cannot enforce a constraint across servers, so we keep the models that refer to each other together.

Migrating multiple databases

The makemigrations command creates the migration of the new app, which does not depend on the database. The migrate command works on one database at a time and applies to the default database unless we pass the --database option. The router controls which operations run. The first command applies the app analytics on the default database: the router forbids it, so Django records the migration as applied but creates no table. The second command applies it on the analytics database, where the table is created. A real project runs plain migrate once for each alias; we name the app to keep the output short. The last two commands list the tables of both databases.

$ uv run python manage.py makemigrations analytics
Migrations for 'analytics':
  analytics/migrations/0001_initial.py
    + Create model PageView
$ uv run python manage.py migrate analytics
Operations to perform:
  Apply all migrations: analytics
Running migrations:
  Applying analytics.0001_initial... OK
$ uv run python manage.py migrate analytics --database=analytics
Operations to perform:
  Apply all migrations: analytics
Running migrations:
  Applying analytics.0001_initial... OK
$ docker exec djpg psql -U shop -d shop -c "\dt analytics*"
Did not find any tables named "analytics*".
$ docker exec djpg psql -U shop -d analytics -c "\dt"
               List of tables
 Schema |        Name        | Type  | Owner 
--------+--------------------+-------+-------
 public | analytics_pageview | table | shop
 public | django_migrations  | table | shop
(2 rows)

Using multiple databases

With the router in place, the code does not change: the QuerySet manager of a model has an attribute db with the alias that the router chose. To override the router, we call using() on a QuerySet or save(using=...) on an instance. The connection of an alias is available as connections['alias'].

demos/multi.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.db import connections
from django.db.utils import DatabaseError

from analytics.models import PageView
from shop.models import Product

Product.objects.all().delete()
PageView.objects.all().delete()

Product.objects.create(name='Lamp', price='19.90')
for path in ('/', '/lamp/', '/'):
    PageView.objects.create(path=path)

print('products:', Product.objects.count(), 'from', Product.objects.db)
print('page views:', PageView.objects.count(), 'from', PageView.objects.db)

for alias in ('default', 'analytics'):
    connection = connections[alias]
    tables = [t for t in connection.introspection.table_names()
              if t.startswith(('shop_', 'analytics_'))]
    print(f'{alias} ({connection.settings_dict["NAME"]}):', tables)

try:
    PageView.objects.using('default').count()
except DatabaseError as error:
    print(type(error).__name__, str(error).splitlines()[0])

The script saves a product and three page views with the ordinary calls. The router sends the products to default and the page views to analytics, which the db attribute confirms. The introspection lists the application tables in each database: each of them holds only its own table. Forcing the model PageView to the default database with using() fails, because the table does not exist there. Transactions are per database as well: transaction.atomic(using='analytics') covers only that database, and there is no transaction that spans both.

$ DJANGO_SETTINGS_MODULE=config.settings_multi uv run python -m demos.multi
products: 1 from default
page views: 3 from analytics
default (shop): ['shop_product']
analytics (analytics): ['analytics_pageview']
ProgrammingError relation "analytics_pageview" does not exist

Test databases

The test runner never touches the databases that we configured. For each database it creates a new one with the name prefixed with test_, runs the migrations in it, and destroys it at the end. The TEST dictionary of a configuration can set another name, a character set, or a mirror of another alias. The user must have the privilege to create databases; in the MariaDB container the user shop owns only the database shop, so we granted it the privileges on the databases whose names begin with test_. A test case may use only the databases that it lists in the databases attribute; by default, that is default, which keeps the tests fast.

shop/tests.py
from django.test import TestCase

from .models import Product


class ProductTests(TestCase):

    def test_create_product(self):
        Product.objects.create(name='Lamp', price='19.90')
        self.assertEqual(Product.objects.count(), 1)

The test of the shop app creates a product and needs only the default database.

analytics/tests.py
from django.test import TestCase

from .models import PageView


class RoutingTests(TestCase):
    databases = {'default', 'analytics'}

    def test_page_views_use_the_analytics_database(self):
        PageView.objects.create(path='/')
        self.assertEqual(PageView.objects.db, 'analytics')
        self.assertEqual(PageView.objects.using('analytics').count(), 1)

The test RoutingTests declares both aliases, creates a page view, and checks that it went to the analytics database. We run the tests with the multi-database settings, which creates two test databases, and then the tests of the shop app on MariaDB.

$ uv run python manage.py test --settings=config.settings_multi
Found 2 test(s).
Creating test database for alias 'default'...
Creating test database for alias 'analytics'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s

OK
Destroying test database for alias 'default'...
Destroying test database for alias 'analytics'...
$ uv run python manage.py test shop --settings=config.settings_mariadb
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK
Destroying test database for alias 'default'...

Source

Django documentation: Databases

Django documentation: Multiple databases

Django documentation: DATABASES setting

In this article we have configured Django for SQLite, PostgreSQL, and MariaDB, tuned SQLite with OPTIONS, used persistent connections, health checks, and a connection pool, and split a project into two databases with a router. Related articles are Django models, Django migrations, and Django raw SQL and transactions.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Django tutorials.