Working with Multiple Databases in Django: Configuration, Migrations, Routing, and Database Routers

This article explains how Django supports multiple databases, including how to define database connections, run migrations on different databases, use management commands with database selection, and implement automatic database routing. It also covers the structure and behavior of database routers, how Django decides which database to use for reads, writes, relations, and migrations, and how hints influence routing decisions.

multiple databases, database routing, database routers, db_for_readdb_for_write, allow_migrateallow_relation, Django ORM

~13 min read • Updated Mar 10, 2026

1. Introduction

Django is designed to work with a single database by default, but it also provides full support for projects that need to interact with multiple databases. When using more than one database, you must configure them explicitly and optionally define routing rules to control how queries are distributed.

2. Defining Multiple Databases

All database connections are defined in the DATABASES setting. Each entry is identified by an alias. The alias default has special meaning: Django uses it whenever no database is explicitly selected.

Example with two databases:


DATABASES = {
    "default": {
        "NAME": "app_data",
        "ENGINE": "django.db.backends.postgresql",
        "USER": "postgres_user",
        "PASSWORD": "s3krit",
    },
    "users": {
        "NAME": "user_data",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "priv4te",
    },
}

Using Django without a meaningful default database

If your project doesn’t logically have a “default” database, you can leave the default entry empty:


DATABASES = {
    "default": {},
    "users": {...},
    "customers": {...},
}

In this case, you must configure DATABASE_ROUTERS so that no queries are accidentally routed to default.

If you try to access a database alias not defined in DATABASES, Django raises ConnectionDoesNotExist.

3. Synchronizing Databases with Migrations

The migrate command operates on one database at a time. By default, it targets default, but you can specify another database:


./manage.py migrate --database=users

To synchronize all databases, run migrate once per database.

If default is empty, you must always specify --database or Django will raise an error.

4. Other Management Commands

Most commands that interact with the database accept --database. The exception is makemigrations, which checks migration history only on the default database unless routers specify otherwise.

5. Automatic Database Routing

Django includes a default routing scheme that:

  • keeps objects “sticky” to the database they were loaded from
  • falls back to default when no database is specified

To implement custom routing logic, you can define your own database routers.

6. Database Routers

A database router is a class that can define up to four methods:

6.1 db_for_read(model, **hints)

Returns the alias of the database to use for read operations.

6.2 db_for_write(model, **hints)

Returns the alias of the database to use for write operations.

6.3 allow_relation(obj1, obj2, **hints)

Determines whether a relation between two objects is allowed.

If all routers return None, Django only allows relations within the same database.

6.4 allow_migrate(db, app_label, model_name=None, **hints)

Controls whether a migration operation is allowed on a given database.

This method is also used to determine whether a model should exist on a particular database.

If allow_migrate() returns False, migration operations for that model will be skipped on that database.

7. Hints

Routers receive a hints dictionary that provides extra context. Currently, the main hint is:

  • instance — the model instance involved in the operation

Routers can use this to route queries based on object state or relationships.

8. Example Router


class UserRouter:
    def db_for_read(self, model, **hints):
        if model._meta.app_label == "users":
            return "users"
        return None

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

    def allow_relation(self, obj1, obj2, **hints):
        if obj1._state.db == obj2._state.db:
            return True
        return False

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == "users":
            return db == "users"
        return None

Conclusion

Django provides robust support for multiple databases through explicit configuration, per-database migrations, and flexible routing via database routers. By defining custom routing logic, you can control where reads, writes, relations, and migrations occur, enabling complex multi-database architectures.

1. Installing and Using Routers

Routers are activated through the DATABASE_ROUTERS setting. This setting contains a list of router class paths. Django’s base router consults these routers whenever it needs to determine which database should handle a query.

When a query is executed, Django:

  1. Calls each router in order.
  2. Uses the first router that returns a database alias.
  3. If none return a suggestion, Django checks instance._state.db from hints.
  4. If no hint is available, Django falls back to the default database.

2. Example: Multi‑Database Architecture

This example uses:

  • A dedicated database for auth and contenttypes
  • A primary/replica setup for all other apps

Database configuration:


DATABASES = {
    "default": {},
    "auth_db": {...},
    "primary": {...},
    "replica1": {...},
    "replica2": {...},
}

3. Router for auth and contenttypes

This router ensures that all auth‑related models are stored and queried only from auth_db.


class AuthRouter:
    route_app_labels = {"auth", "contenttypes"}

    def db_for_read(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return "auth_db"
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return "auth_db"
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if (
            obj1._meta.app_label in self.route_app_labels
            or obj2._meta.app_label in self.route_app_labels
        ):
            return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label in self.route_app_labels:
            return db == "auth_db"
        return None

4. Router for primary/replica setup

This router:

  • Randomly distributes read queries across replicas
  • Sends all writes to the primary database
  • Allows relations only within the primary/replica group

class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        return random.choice(["replica1", "replica2"])

    def db_for_write(self, model, **hints):
        return "primary"

    def allow_relation(self, obj1, obj2, **hints):
        db_set = {"primary", "replica1", "replica2"}
        if obj1._state.db in db_set and obj2._state.db in db_set:
            return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        return True

5. Registering Routers


DATABASE_ROUTERS = [
    "path.to.AuthRouter",
    "path.to.PrimaryReplicaRouter",
]

The order matters. Routers are evaluated in the order listed. In this example, AuthRouter must come first so that auth models are routed correctly.

6. Real‑World Example of Query Routing


# Read from auth_db
fred = User.objects.get(username="fred")
fred.first_name = "Frederick"
fred.save()  # also goes to auth_db

# Read from a random replica
dna = Person.objects.get(name="Douglas Adams")

# New object has no assigned database yet
mh = Book(title="Mostly Harmless")

# Assigning a relation binds mh to the same database as dna
mh.author = dna

# Saving forces mh onto the primary database
mh.save()

# Re-fetching will read from a replica
mh = Book.objects.get(title="Mostly Harmless")

7. Manual Database Selection

You can override router decisions by manually selecting a database using using(). This always takes priority over router logic.


Author.objects.all()              # runs on default
Author.objects.using("default")   # explicitly default
Author.objects.using("other")     # runs on 'other' database

Conclusion

Django’s database routers provide powerful control over multi‑database environments. By defining custom routing logic, you can direct reads, writes, relations, and migrations to the appropriate databases. Combined with manual selection using using(), Django enables flexible and scalable multi‑database architectures.

1. Selecting a Database for save()

You can specify the database on which a model instance should be saved by passing the using keyword argument to save():


my_object.save(using="legacy_users")

If you don’t specify using, Django will save the object to the database chosen by the routers (usually default).

2. Moving an Object Between Databases

Saving an object to a different database may seem like a way to “move” it, but this can cause issues due to primary key reuse.

Example:


p = Person(name="Fred")
p.save(using="first")     # INSERT → assigns primary key
p.save(using="second")    # Uses the same primary key on second DB

If the primary key already exists in the second database, the existing record will be overwritten.

Two safe solutions:

Solution 1: Clear the primary key


p = Person(name="Fred")
p.save(using="first")
p.pk = None
p.save(using="second")  # Creates a new object safely

Solution 2: Use force_insert


p = Person(name="Fred")
p.save(using="first")
p.save(using="second", force_insert=True)

This forces Django to perform an INSERT. If the primary key already exists, an error is raised instead of overwriting data.

3. Selecting a Database for delete()

By default, delete() runs on the database from which the object was retrieved:


u = User.objects.using("legacy_users").get(username="fred")
u.delete()   # Deletes from legacy_users

You can override this by passing using to delete():


user_obj.save(using="new_users")
user_obj.delete(using="legacy_users")

4. Using Managers with Multiple Databases

Manager methods (like create_user()) cannot be routed using using() because they are not QuerySet methods.

Instead, use db_manager() to bind a manager to a specific database:


User.objects.db_manager("new_users").create_user(...)

db_manager() returns a copy of the manager bound to the database you specify.

5. Overriding get_queryset() in Multi‑Database Managers

If you override get_queryset(), you must ensure that the manager respects the database it is bound to. Use self._db to apply the correct database:


class MyManager(models.Manager):
    def get_queryset(self):
        qs = CustomQuerySet(self.model)
        if self._db is not None:
            qs = qs.using(self._db)
        return qs

This ensures that the manager behaves correctly when used with db_manager() or when Django binds it to a specific database through routing.

Conclusion

Django provides fine‑grained control over database selection for saving, deleting, and manager operations. Understanding how primary keys behave across databases, how to use db_manager(), and how to correctly implement get_queryset() ensures safe and predictable behavior in multi‑database environments.

1. Django Admin and Multiple Databases

Django’s admin interface does not natively support multiple databases. If you want to manage models stored on a non‑default database, you must customize ModelAdmin to explicitly route admin operations to the correct database.

2. Customizing ModelAdmin for Multi‑Database Support

To direct admin operations (save, delete, queryset, form fields) to a specific database, override the following methods:


class MultiDBModelAdmin(admin.ModelAdmin):
    using = "other"

    def save_model(self, request, obj, form, change):
        obj.save(using=self.using)

    def delete_model(self, request, obj):
        obj.delete(using=self.using)

    def get_queryset(self, request):
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

This approach assumes all objects of a given model live on the same database. More complex setups require more advanced logic.

3. Customizing InlineModelAdmin

Inline admin classes also need to be customized:


class MultiDBTabularInline(admin.TabularInline):
    using = "other"

    def get_queryset(self, request):
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

4. Registering Multi‑Database Admin Classes


class BookInline(MultiDBTabularInline):
    model = Book

class PublisherAdmin(MultiDBModelAdmin):
    inlines = [BookInline]

admin.site.register(Author, MultiDBModelAdmin)
admin.site.register(Publisher, PublisherAdmin)

othersite = admin.AdminSite("othersite")
othersite.register(Publisher, MultiDBModelAdmin)

This setup creates two admin sites: one showing Authors and Publishers (with inline Books), and another showing only Publishers.

5. Using Raw Cursors with Multiple Databases

To execute raw SQL on a specific database, use django.db.connections:


from django.db import connections

with connections["my_db_alias"].cursor() as cursor:
    ...

6. Limitations of Multi‑Database Setups

6.1 Cross‑Database Relations

Django does not support foreign key or many‑to‑many relationships across databases. All relational constraints must remain within a single database.

Databases like PostgreSQL, SQLite, Oracle, and MySQL/InnoDB enforce this at the database level. MyISAM does not enforce referential integrity, but Django still does not support cross‑database relations.

6.2 Behavior of contrib Apps

Some contrib apps depend on each other and must be stored together:

  • auth, contenttypes, permissions → must be in the same database
  • admin → depends on auth
  • flatpages and redirects → depend on sites

Additionally, after migrate, Django automatically creates:

  • a default Site
  • a ContentType for every model
  • Permissions for every model

In multi‑database setups, these objects are usually needed only in one database. A router should be written to ensure they sync only to a single database.

Warning: If ContentTypes are synced to multiple databases, their primary keys may differ, causing data corruption.

Conclusion

Django Admin does not automatically handle multiple databases, but with custom ModelAdmin and InlineAdmin classes, you can fully control where data is read from and written to. Understanding the limitations—especially the lack of cross‑database relations and the constraints of contrib apps—is essential for building a stable multi‑database architecture.

Written & researched by Dr. Shahin Siami