skip to content

In Django, what do a database router's db_for_read, db_for_write, allow_relation and allow_migrate methods decide, and what does returning None mean?

level: middleimportance: should knowfreq 38%

answer

  1. a chain of plain classes
  2. first answer wins, None passes
  3. order in the settings list
  4. instance hint before default
  5. same-database rule for relations

basics

~20 s

Routers listed in DATABASE_ROUTERS pick an alias for reads and writes, approve relations and approve migrations. Returning None defers to the next router; if none answers, Django uses the hint instance's database or 'default', allows same-database relations and allows migrations.

solid answer

~40 s

A router is any class with some of four methods, listed by dotted path in `DATABASE_ROUTERS` and tried in order. `db_for_read(model, **hints)` and `db_for_write(model, **hints)` return an alias; the only hint Django currently passes is `instance`. `allow_relation(obj1, obj2, **hints)` returns True or False for assigning a related object; False raises `ValueError`. `allow_migrate(db, app_label, model_name=None, **hints)` says whether migrations may touch that model on that alias. Returning `None`, or omitting the method, passes to the next router. When all pass, reads and writes use the hint instance's `_state.db` or `'default'`, relations are allowed only between objects from the same database, and migrations are allowed.

code

python · 20 lines
python
# settings.py
DATABASE_ROUTERS = ['marketplace.routers.LedgerRouter', 'marketplace.routers.PoolRouter']


# marketplace/routers.py
class LedgerRouter:
    app_label = 'ledger'

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

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

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == self.app_label:
            return db == 'ledger'
        if db == 'ledger':
            return False
        return None

go deeper

for a junior

Know that routers are classes listed in DATABASE_ROUTERS, and that db_for_read and db_for_write return an alias name for a model.

for a middle

Explain the chain: list order, None or a missing method passes on, then the instance's _state.db, then 'default'; and what allow_relation and allow_migrate default to.

for a senior

Show you can write routers that compose: narrow first, None for unowned models, instance hints respected, and allow_migrate reading only _meta on historical models.

for a principal

Weigh putting database placement in routers against explicit using() in services, and decide who owns the router file when several teams share one project.

## What a router is A **database router** is a plain Python class; it does not subclass anything. Django instantiates each entry of the `DATABASE_ROUTERS` setting (a list of dotted paths, empty by default) and wraps them in the base router, `django.db.router`. Whenever the ORM needs a decision, it asks the base router, which asks your routers **in list order**. A router may implement any subset of four methods. A missing method is treated exactly like an answer of `None`: that router is skipped for that decision. ## The four methods | Method | Called when | Returns | If every router passes | |---|---|---|---| | `db_for_read(model, **hints)` | a QuerySet read runs without `using()` | an alias or `None` | hint instance's `_state.db`, else `'default'` | | `db_for_write(model, **hints)` | a save, delete, create, `update()`, `get_or_create()` or `select_for_update()` runs without `using()` | an alias or `None` | hint instance's `_state.db`, else `'default'` | | `allow_relation(obj1, obj2, **hints)` | a related object is assigned or added | `True`, `False` or `None` | allowed only if both objects share `_state.db` | | `allow_migrate(db, app_label, model_name=None, **hints)` | `migrate` considers an operation on alias `db` | `True`, `False` or `None` | allowed | For `db_for_read` and `db_for_write`, the **first truthy alias wins**. For `allow_relation` and `allow_migrate`, the **first non-`None` answer wins**, so an explicit `False` stops the chain. ## Hints The `hints` dictionary carries extra context. For reads and writes the only hint Django currently supplies is **`instance`**: the object being saved, or the object whose related manager is running the query. Its `instance._state.db` records the database it was loaded from. A router that wants Django's default behaviour of fetching related objects from the same database should check that attribute before returning a fixed alias. For `allow_migrate`, when `model_name` is set the hints usually include `'model'`, which may be a **historical model** from the migration state: only its `_meta` is reliable, not your custom methods or managers. ## What allow_relation protects Django does not support foreign keys or many-to-many relations spanning two databases, because the database cannot enforce the key. `allow_relation` is the guard at assignment time: `book.author = author` calls it, and if the answer is `False` the descriptor raises `ValueError` with *the current database router prevents this relation*. A primary/replica pool is the usual reason to override the default, since an object read from `'replica'` and one from `'default'` hold the same data: ```python def allow_relation(self, obj1, obj2, **hints): pool = {'default', 'replica'} if obj1._state.db in pool and obj2._state.db in pool: return True return None ``` ## How one read is resolved Take `Listing.objects.filter(active=True)` evaluated in a view, with two routers installed and no `using()`: 1. The QuerySet is not marked for writing, so Django calls `router.db_for_read(Listing, **hints)`. 2. The base router asks the first router. It has no `db_for_read` method, so it is skipped. 3. The second router returns `None` because `Listing` is not one of its models. 4. No router answered and there is no `instance` hint, so the base router returns `'default'`. If the same QuerySet had come from a related manager, such as `seller.listings.all()`, the seller would be the `instance` hint and step 4 would return the seller's `_state.db` instead. The resolution happens when the query runs, not when the QuerySet is built. Because routers are plain classes, they are easy to unit-test without a database: instantiate the router and call `db_for_read(Listing)` or `allow_migrate('replica', 'shop', model_name='listing')` directly, asserting on the returned alias or verdict. ## Why order matters - Put **narrow** routers first (one app to its own database) and **catch-all** routers last. A catch-all whose `allow_migrate` returns `True` for everything, listed first, makes every model migrate onto every alias. - Return `None`, not `'default'`, for models a router does not own, or the routers after it never get a say. - `DATABASE_ROUTERS` is read once, so routers should be stateless or read request-scoped state themselves. ## Common mistakes 1. Returning `False` from `allow_migrate` or `allow_relation` to mean 'not my model': there it is a verdict that ends the chain, and only `None` passes to the next router. 2. Ignoring the `instance` hint, so related lookups on an object loaded from one database go to another. 3. Forgetting that a manual `using()` bypasses the router entirely, then debugging the router for a query it never saw.

  • Why should a router check hints['instance']._state.db before returning a fixed alias?
    When code follows a relation, such as `book.author`, Django passes the object as the `instance` hint. Its `_state.db` says which database it came from. Returning that alias keeps related lookups on the same database, which is what Django does when no router answers; a router that ignores it can read the related row from a database that lacks it.
  • What goes wrong if a catch-all router is listed before an app-specific one?
    The base router stops at the first answer. A catch-all that returns an alias for every read and write, or `True` from `allow_migrate` for every model, answers before the specific router is asked, so that app's models land on the catch-all's database and its tables get created on every alias.

A router chain works like a mailroom staffed by clerks in a fixed order: each clerk either writes a destination on the envelope or passes it on untouched. The first clerk who writes one decides; if nobody does, the envelope goes back to the office on its return address, or to head office when it has none.

saying these in an interview costs you the question

  • Routers must subclass a Django base router class to be picked up.
  • Every router in DATABASE_ROUTERS votes and the majority alias wins.
  • When no router answers, Django picks a random alias from DATABASES.
  • allow_relation lets a ForeignKey safely point into another database.
  • Returning 'default' for unowned models is harmless in a chain of routers.