Skip to content

Database strategies

A database strategy decides how a tenant's database is reached, created and dropped. Each Client names its strategy in db_strategy; the strategy reads its parameters from the client's encrypted db_params and its db_name.

db_strategy is a StrategyField from django-strategy-field: the column stores the dotted path of a class, and reading the attribute returns an instance bound to the client:

client.db_strategy  # <TemplateOverlay object>, built as TemplateOverlay(client)
client.db_strategy.client is client  # True
client.get_db_strategy()  # same, but raises ImproperlyConfigured if the class cannot be imported

Who calls the strategy

Caller Strategy method
Client.clean() (admin, full_clean(), provision_tenant) validate()
ensure_db_registered() (middleware, tenant_context(), migrate_tenant()) connection_settings(template)
provisioning.create_database() / provision() create_database(if_not_exists)
provisioning.drop_database() drop_database()

ensure_db_registered() always resets TEST["NAME"] to None on what the strategy returns, so no strategy can make a test run reuse the template's test database.

TemplateOverlay

The built-in strategy, always registered and the default for every client: the template alias connection with the client's parameters laid over it. Its db_params are shaped like a Django DATABASES entry without NAME, which comes from client.db_name.

db_params key Effect
ENGINE, HOST, PORT, USER, PASSWORD Replace the template's value
OPTIONS (object) Merged into the template's OPTIONS
CONN_MAX_AGE, CONN_HEALTH_CHECKS Replace the template's value
MAINTENANCE_DB PostgreSQL only: the database to connect to for CREATE / DROP DATABASE. Default postgres

Missing keys, empty strings and null inherit the template's value. NAME is always client.db_name. Any other key fails validation:

{'db_params': ['Template connection + overrides does not accept NAME. Allowed keys: CONN_HEALTH_CHECKS, ..., PORT, USER.']}

Examples of db_params:

{}
{"HOST": "db2.internal", "USER": "acme", "PASSWORD": "s3cret", "OPTIONS": {"sslmode": "require"}}
{"ENGINE": "django.db.backends.mysql", "HOST": "mysql.internal", "PORT": "3306"}

Creating and dropping databases uses the resulting connection: its server and its account, which needs the right to create and drop databases.

Vendor create_database() drop_database()
PostgreSQL Connects to MAINTENANCE_DB in autocommit, checks pg_database, CREATE DATABASE. Returns False when it already exists (RuntimeError with if_not_exists=False) DROP DATABASE IF EXISTS
MySQL Connects without a default database, CREATE DATABASE [IF NOT EXISTS] ... CHARACTER SET utf8mb4 DROP DATABASE IF EXISTS
SQLite Nothing: the file appears on first connection Deletes the file
Others NotImplementedError NotImplementedError

Write your own strategy

Subclass DatabaseStrategy (or TemplateOverlay to keep its behaviour), declare the db_params keys it accepts, and register it.

Example: take the password from an environment variable named in db_params, so the secret is not stored in the database:

# myproject/tenancy.py
import os

from django.core.exceptions import ImproperlyConfigured
from django_db_tenants.strategies import TemplateOverlay


class EnvironmentPassword(TemplateOverlay):
    label = "Template + password from environment"
    param_keys = TemplateOverlay.settings_keys - {"PASSWORD"} | {"PASSWORD_ENV", "MAINTENANCE_DB"}

    def connection_settings(self, template):
        settings = super().connection_settings(template)
        variable = self.params.get("PASSWORD_ENV")
        if variable:
            try:
                settings["PASSWORD"] = os.environ[variable]
            except KeyError:
                raise ImproperlyConfigured(f"{variable} is not set") from None
        return settings
# settings.py
DB_TENANTS = {"DATABASE_STRATEGIES": ["myproject.tenancy.EnvironmentPassword"]}
python manage.py provision_tenant acme --name ACME --db-name acme \
    --db-strategy myproject.tenancy.EnvironmentPassword --db-params '{"PASSWORD_ENV": "ACME_DB_PASSWORD"}'

The test suite ships this exact strategy (tests/_extra/testutils/strategies.py).

The contract

Member Required Contract
label recommended Name in the admin select and in error messages
param_keys recommended frozenset of accepted db_params keys; None accepts anything
validate() no Raise ValidationError({"db_params": ...}) for bad parameters; call super().validate()
connection_settings(template) yes Return the complete DATABASES entry. template is a private deep copy of the template alias's settings with Django's defaults filled in: modify it or build a new dict. Called on every (re-)registration, so keep it fast; it may run in any thread
create_database(if_not_exists=True) for provisioning Create the database; return True when created
drop_database() for dropping Drop the database if it exists

Inside a strategy, self.client is the Client and self.params its db_params (a dict).

The DbConnectionFactory protocol declares these members. Client.get_db_strategy() is typed with it, so the callers above depend on the interface, not on DatabaseStrategy. Strategies still subclass DatabaseStrategy: the db_strategy field accepts only its registered subclasses.

Registration

The Client.db_strategy field accepts only registered classes, both in the admin select and in full_clean().

Way When
DB_TENANTS["DATABASE_STRATEGIES"] = ["dotted.Path"] Registered when the app is ready. Use this in projects
strategies.registry.register(MyStrategy) From code, for example in your own AppConfig.ready()

Registering a strategy never creates a migration: the field's choices are not part of the schema.

When a strategy class disappears

A client row whose strategy class cannot be imported still loads (lists, admin), with db_strategy set to None. Using it raises ImproperlyConfigured naming the missing class, and full_clean() reports the field as invalid. Keep removed strategies importable until no client uses them.

Changing a client's strategy or parameters

Saving the client changes its config version, so every process re-registers the connection with the new settings the next time it meets the client.

django_db_tenants.strategies

Database strategies: how a tenant's database is reached, created and dropped.

Every Client names its strategy in Client.db_strategy, a django-strategy-field StrategyField holding the dotted path of a :class:DatabaseStrategy subclass from :data:registry. Reading client.db_strategy returns an instance bound to the client.

A strategy reads its parameters from client.db_params (an encrypted JSON object whose keys the strategy defines) and client.db_name. Register your own strategies with registry.register(MyStrategy) or DB_TENANTS["DATABASE_STRATEGIES"].

DEFAULT_STRATEGY module-attribute

DEFAULT_STRATEGY = (
    "django_db_tenants.strategies.TemplateOverlay"
)

registry module-attribute

registry = Registry(
    DatabaseStrategy, label_attribute="label"
)

DatabaseStrategy

Base class of database strategies.

Subclasses implement :meth:connection_settings and, to support provisioning, :meth:create_database and :meth:drop_database.

label class-attribute instance-attribute

label = ''

param_keys class-attribute instance-attribute

param_keys: frozenset[str] | None = None

client instance-attribute

client = client

params property

params: dict[str, Any]

The client's db_params, or an empty dict.

validate

validate() -> None

Check client.db_params; raise ValidationError on bad values. Called by Client.clean().

connection_settings

connection_settings(
    template: dict[str, Any],
) -> dict[str, Any]

Return the DATABASES entry of the tenant's database.

template is a private deep copy of the template alias's settings (complete, with Django's defaults filled in): modify and return it, or build a new dict.

create_database

create_database(if_not_exists: bool = True) -> bool

Create the tenant's database. Return True if it was created.

drop_database

drop_database() -> None

Drop the tenant's database if it exists.

TemplateOverlay

Bases: DatabaseStrategy

The template connection with the client's parameters laid over it.

db_params keys are DATABASES keys: ENGINE, HOST, PORT, USER, PASSWORD, OPTIONS (merged into the template's), CONN_MAX_AGE, CONN_HEALTH_CHECKS, plus MAINTENANCE_DB: the PostgreSQL database to connect to for CREATE / DROP DATABASE (default postgres). Missing, empty and null values inherit the template's. NAME is client.db_name.

Databases are created and dropped on the server and with the account of the resulting connection, which needs the right to do so.

label class-attribute instance-attribute

label = 'Template connection + overrides'

settings_keys class-attribute instance-attribute

settings_keys = frozenset(
    {
        "ENGINE",
        "HOST",
        "PORT",
        "USER",
        "PASSWORD",
        "OPTIONS",
        "CONN_MAX_AGE",
        "CONN_HEALTH_CHECKS",
    }
)

param_keys class-attribute instance-attribute

param_keys = settings_keys | {'MAINTENANCE_DB'}

validate

validate() -> None

connection_settings

connection_settings(
    template: dict[str, Any],
) -> dict[str, Any]

create_database

create_database(if_not_exists: bool = True) -> bool

drop_database

drop_database() -> None

template_settings

template_settings() -> dict[str, Any]

A private copy of the template alias's connection settings.