Мультибазность в Django: несколько БД в одном проекте
Как развести чтение и запись по разным базам, направить часть моделей в отдельную БД и не словить ошибку при джойне между базами. Разбор роутеров, миграций и граничных случаев на Django 5.x.

Django из коробки умеет работать с несколькими базами данных одновременно: одну назначить основной, вторую — под аналитику, третью — под реплику для чтения. Проблема в том, что фреймворк не решает за вас, какой запрос в какую базу отправить. Как только вы добавляете второй ключ в DATABASES, ORM продолжает лить всё в default, пока вы явно не напишете правила маршрутизации. А при попытке сджойнить модель из одной базы с моделью из другой вы получите либо тихо неверный результат, либо ошибку целостности на уровне БД.
Разберём, как настроить несколько баз, написать роутер, развести миграции и где эта схема ломается. Актуально для Django 5.x, но сам механизм DATABASE_ROUTERS не менялся с версии 1.2 — принципиально ничего нового давно не появлялось.
Что настраивается в settings
Базы описываются в словаре DATABASES. Ключ default обязателен: если вы не собираетесь его использовать, Django всё равно требует его наличия, но можно оставить пустой словарь {}. Остальные ключи — произвольные имена, на которые вы будете ссылаться в роутерах и в using().
# settings.py — Django 5.x
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "main",
"USER": "app",
"PASSWORD": "YOUR_DB_PASSWORD",
"HOST": "127.0.0.1",
"PORT": "5432",
},
# отдельная база под аналитику
"analytics": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "analytics",
"USER": "app",
"PASSWORD": "YOUR_DB_PASSWORD",
"HOST": "127.0.0.1",
"PORT": "5432",
},
# реплика только для чтения
"replica": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "main",
"USER": "readonly",
"PASSWORD": "YOUR_DB_PASSWORD",
"HOST": "replica.internal",
"PORT": "5432",
},
}После этого любой запрос по-прежнему идёт в default. Направить его в другую базу можно двумя способами: явно через using() для разового обращения или через роутер для правил на весь проект.
Явное указание базы
Самый предсказуемый способ — QuerySet.using() и менеджер objects.using(). Django ничего не угадывает, вы полностью контролируете направление.
# чтение с реплики
users = User.objects.using("replica").filter(is_active=True)
# запись в аналитику
Event.objects.using("analytics").create(name="signup", user_id=42)
# сохранение конкретного объекта в конкретную базу
report.save(using="analytics")Этот способ хорош для точечных операций. Но если у вас десятки мест, где нужно читать с реплики, обвешивать каждый вызов using() — путь к тому, что кто-то забудет и уронит нагрузку на мастер. Тогда нужен роутер.
Роутер базы данных
Роутер — это класс с четырьмя методами. Django вызывает их, чтобы решить, куда отправить операцию. Каждый метод либо возвращает имя базы, либо None — тогда Django спрашивает следующий роутер в списке, а если все вернули None, использует default.
db_for_read(model, **hints)— куда читать;db_for_write(model, **hints)— куда писать;allow_relation(obj1, obj2, **hints)— можно ли связать два объекта отношением;allow_migrate(db, app_label, model_name=None, **hints)— какие миграции применять в какой базе.
# db_routers.py
class AnalyticsRouter:
"""Всё из приложения analytics живёт в отдельной базе."""
route_app_labels = {"analytics"}
def db_for_read(self, model, **hints):
if model._meta.app_label in self.route_app_labels:
return "analytics"
return None
def db_for_write(self, model, **hints):
if model._meta.app_label in self.route_app_labels:
return "analytics"
return None
def allow_relation(self, obj1, obj2, **hints):
labels = self.route_app_labels
if obj1._meta.app_label in labels or obj2._meta.app_label in labels:
# разрешаем связь, только если обе модели из analytics
return (
obj1._meta.app_label in labels
and obj2._meta.app_label in labels
)
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label in self.route_app_labels:
# таблицы analytics создаём только в базе analytics
return db == "analytics"
# прочие приложения в analytics не мигрируем
if db == "analytics":
return False
return NoneПодключается список роутеров в настройках. Порядок важен: Django опрашивает их сверху вниз и берёт первый непустой ответ.
# settings.py
DATABASE_ROUTERS = ["myproject.db_routers.AnalyticsRouter"]Роутер для реплики чтения
Частый сценарий: писать в мастер, читать со слейва. Здесь db_for_read возвращает реплику, а db_for_write — default.
import random
class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
# если реплик несколько — можно балансировать
return random.choice(["replica"])
def db_for_write(self, model, **hints):
return "default"
def allow_relation(self, obj1, obj2, **hints):
# мастер и реплика — физически одни и те же данные
db_set = {"default", "replica"}
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):
# на реплику миграции не льём — она read-only
return db != "replica"Роутер решает, куда отправить запрос, но не гарантирует, что данные там уже есть. Между записью в мастер и появлением строки на реплике всегда есть задержка репликации. Если сразу после create() читать со слейва, вы можете не увидеть только что записанное.Миграции по разным базам
Команда migrate по умолчанию работает только с default. Чтобы применить миграции к другой базе, укажите её флагом --database:
python manage.py migrate --database=analyticsМетод allow_migrate при этом решает, какие приложения физически создать в этой базе. Без него Django попытается создать все таблицы во всех базах, и вы получите дубли: таблицы приложения analytics появятся и в default, и наоборот.
Проверка после миграции: подключитесь к базе analytics напрямую (psql, \dt) и убедитесь, что там есть только таблицы приложения analytics и служебная django_migrations, а таблиц из основного приложения нет.
Где схема ломается
Мультибазность в Django — это не распределённая транзакция и не шардинг. Ограничения жёсткие, и большинство инцидентов приходится именно на них.
| Ситуация | Что происходит |
|---|---|
| ForeignKey между моделями в разных базах | На уровне БД внешнего ключа нет, целостность не проверяется. Джойн через ORM невозможен, select_related не сработает между базами. |
transaction.atomic() на две базы | Атомарность в пределах одной базы. Ошибка во второй базе не откатит первую — распределённых транзакций нет. |
| Чтение сразу после записи с реплики | Лаг репликации: строки может ещё не быть. Для read-after-write читайте с мастера явно через using("default"). |
| Join между таблицами разных БД в одном запросе | СУБД не видит вторую базу — запрос упадёт или вернёт неверный результат. Данные приходится собирать в Python. |
Внешние ключи между базами
Django позволит объявить ForeignKey на модель из другой базы, но на уровне PostgreSQL это будет обычное поле-число без ограничения ссылочной целостности. Удалите строку в мастере — «висячая» ссылка в аналитике останется, и никакой on_delete её не тронет, потому что каскад работает только внутри одной базы. Если связь между разнесёнными моделями действительно нужна, храните идентификатор как обычное поле и собирайте данные двумя запросами.
Транзакции
Оборачивание в atomic() задаёт транзакцию для одной базы — по умолчанию default. Для другой базы нужен отдельный контекст: with transaction.atomic(using="analytics"):. Но даже два вложенных atomic на разные базы не дадут вам двухфазный коммит: если вторая транзакция упадёт после коммита первой, откатить первую уже нельзя.
Что делать дальше
Сначала решите, зачем вам вторая база: разгрузить чтение репликой, изолировать тяжёлую аналитику или физически разделить данные. От ответа зависит роутер. Для реплики чтения проверьте лаг репликации на стороне PostgreSQL (pg_stat_replication) — если он растёт под нагрузкой, часть чтений придётся вернуть на мастер. Для изоляции приложения убедитесь через allow_migrate, что таблицы не расползлись по базам. И заведите тест, который прогоняет migrate --database= для каждой базы на чистой копии — так вы поймаете расхождение схемы раньше прода.
Подробности по каждому методу роутера и флагам команд смотрите в разделе Multiple databases официальной документации Django.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: Django documentation — Multiple databases
Частые вопросы
А если у меня Django 4.2, роутеры работают так же?
Почему после create() чтение с реплики не находит запись?
Можно ли сделать ForeignKey между моделями в разных базах?
Как заставить migrate применить миграции ко второй базе?
Что делать, если таблицы приложения появились в обеих базах?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.