skip to content

In Django with several DATABASES aliases, how do migrate --database and a router's allow_migrate() decide which tables are created on each database?

level: middleimportance: nice to knowfreq 22%

answer

  1. one database per migrate run
  2. migration files do not depend on routers
  3. operations skipped, migration still recorded
  4. RunPython asks with its own hints
  5. never migrate the replica

basics

~20 s

migrate works on one alias per run, 'default' unless --database names another. For each operation it asks allow_migrate(db, app_label, model_name); a False skips that operation silently, yet the migration is still recorded as applied on that alias.

solid answer

~30 s

`makemigrations` writes migration files for every model change regardless of routers. `migrate` then synchronises **one** alias per run: `'default'` unless you pass `--database=<alias>`, so a second database needs its own `manage.py migrate --database=ledger`. For each schema operation Django calls the routers' `allow_migrate(db, app_label, model_name=..., model=...)`; if the chain answers `False` the operation is skipped silently, while the migration is still recorded in that alias's `django_migrations` table. `RunPython` and `RunSQL` ask with only `app_label` plus whatever `hints` you gave them. A replica should never be migrated: replication copies the primary's schema, and the router should return `False` for it.

code

python · 11 lines
python
class PlacementRouter:
    ledger_apps = {'ledger'}

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if db == 'replica':
            return False  # replication supplies the schema
        if app_label in self.ledger_apps:
            return db == 'ledger'
        if db == 'ledger':
            return False
        return None  # everything else: default rules on 'default'

go deeper

for a junior

Remember that migrate touches only 'default' unless --database names another alias, and that each extra database needs its own run.

for a middle

Explain allow_migrate's three answers, that skipped operations still record the migration, and how RunPython and RunSQL are asked with hints only.

for a senior

Keep replicas out of migrate, route contenttypes and auth to one alias, and treat any change to allow_migrate for existing models as a planned data move.

for a principal

Decide whether a separate database is worth the release complexity it adds: an extra migrate step, split contrib data and no cross-database relations.

## One database per migrate run The `migrate` management command synchronises exactly **one** alias per invocation. Its `--database` option defaults to `'default'` and only accepts aliases defined in `DATABASES`. A project with a separate ledger database therefore runs two commands in its release step: ```bash python manage.py migrate python manage.py migrate --database=ledger ``` If `'default'` is left as an empty dict because routers send everything elsewhere, every `migrate` needs an explicit `--database`. Most other database-facing management commands (`dbshell`, `dumpdata`, `loaddata`, `flush`, `createcachetable`) take the same `--database` option and also operate on one alias at a time. ## Migration files ignore routers `makemigrations` has no `--database` option and writes operations for **every** model change, whatever your routers say. Routers act later, when `migrate` executes those operations against a particular alias. There is one exception: when `DATABASE_ROUTERS` is set, `makemigrations` also checks the migration history on each alias where at least one model is allowed to migrate, which means it opens connections to those databases. ## What allow_migrate() decides For each model operation (`CreateModel`, `AddField`, `AlterField` and the rest) Django asks the routers: ```python allow_migrate(db, app_label, model_name='invoice', model=<historical model>) ``` - **`True`**: the operation runs on this alias. - **`False`**: the operation is **silently skipped** on this alias. - **`None`**: the next router decides; if every router passes, the answer is `True`. Three details catch people out: 1. **The migration is still recorded.** Skipping works per operation, not per migration, so the migration is written to that alias's `django_migrations` table and `showmigrations --database` lists it as applied, even though no table was created. 2. **The model hint is historical.** It comes from the migration state, so only its `_meta` is dependable; custom methods and managers are absent. 3. **Unmanaged, proxy and swapped models** are skipped before the router is even asked. ## RunPython and RunSQL Data migrations have no model of their own. `RunPython` and `RunSQL` call `allow_migrate(db, app_label, **hints)` with no `model_name` of their own, passing only the `hints` dictionary you gave the operation. A router that decides purely on `model_name` therefore sees `None` there and has to fall back to the app label, so pass hints when the router needs them: ```python migrations.RunPython(backfill_totals, hints={'target_db': 'ledger'}) ``` Inside the function, run queries through `schema_editor.connection.alias` rather than the default manager, so the data goes to the alias being migrated. ## Replicas and contrib apps | Alias | Should migrate run on it? | Router answer for it | |---|---|---| | primary (`'default'`) | yes | `True` or `None` | | app-specific database (`'ledger'`) | yes, for its own apps | `True` for its apps, `False` for the rest | | read replica (`'replica'`) | **no** | `False` for everything | A streaming replica receives the primary's schema through replication; running `migrate --database=replica` against it would try to create tables on a read-only server. Returning `False` for the replica also keeps `makemigrations` from checking history there. After `migrate` creates their tables, `contenttypes` and `auth` insert a `ContentType` and the default `Permission` rows for **every** model, including models stored elsewhere. The `auth` models must live on the same database as `contenttypes`, and the `admin` models with them, so route these apps to one alias. ## Changing the rules later Changing what `allow_migrate` returns for a model that already has applied migrations is dangerous. Django records those migrations as applied on each alias whether or not the operations ran, so a model newly allowed on an alias that was migrated before finds its creation migration already marked applied and never gets a table. Plan a new placement like any other data move: create the table deliberately, copy the rows, then switch the router. ## A release step that stays correct 1. Run `makemigrations --check` in CI so no model change ships without its migration file. 2. Run `migrate` once per alias that owns tables, in a fixed order, as part of the release: `'default'` first, then each app-specific database. 3. Never list the replica in that step; if someone runs it by hand, the router's `False` for the replica keeps it from doing anything but touching the history table. 4. After a routing change, run `showmigrations --database=<alias>` for each alias and compare it with the tables that actually exist, since the history can say applied where the schema says otherwise. This keeps the rule simple to explain in a review: the migration files are the same for every database, and the routers decide which of their operations each database receives.

  • Why can a model end up with no table on an alias even though showmigrations --database lists its migrations as applied?
    Routers skip operations, not migrations. When `allow_migrate` returned `False` for the model on that alias, its `CreateModel` was skipped but the migration was still recorded in that alias's `django_migrations` table. Changing the router afterwards does not re-run it, so the table has to be created deliberately.
  • How should a RunPython data migration find the database it is running against?
    Read `schema_editor.connection.alias` and run queries through `Model.objects.using(alias)` on the historical model from `apps.get_model()`. Using the default manager without an alias would go through the routers and could write to a different database from the one being migrated.

saying these in an interview costs you the question

  • makemigrations only writes operations for models the router allows.
  • One migrate run updates every alias listed in DATABASES.
  • When allow_migrate returns False, the migration stays unapplied on that alias.
  • The read replica needs its own migrate --database run on each release.
  • allow_migrate can call custom model methods on the model hint.