Django ORM: модели и миграции без магии (или почти без неё)¶
Когда я первый раз запустил python manage.py migrate, мне показалось, что Django читает мысли. Описал класс → получил таблицу. Поправил поле → база обновилась. Идиллия. Пока не наступает момент, когда миграции начинают конфликтовать в мердже, makemigrations предлагает дропнуть колонку с боевыми данными, а в админке вместо записей красуется Article object (42).
Давайте разберёмся, как на самом деле устроены модели и миграции, и как не превратить их в источник бессонных ночей. Пишу так, как объяснял бы коллеге за кофе, без документации и лишних церемоний.
Модели: не просто таблицы, а контракт¶
Модель в Django — это не обёртка над SQL. Это контракт между вашим кодом и базой данных. Если относиться к ней как к чертежу, а не как к списку полей, живётся гораздо спокойнее.
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
is_published = models.BooleanField(default=False)
published_at = models.DateTimeField(null=True, blank=True)
Казалось бы, всё очевидно. Но вот тут 90% новичков спотыкаются: null=True и blank=True не синонимы.
- null говорит базе: «можно хранить SQL-пустоту».
- blank говорит валидации Django и админке: «можно оставить поле пустым в форме».
Смешивать их без причины — прямой путь к NULL в строках, где их быть не должно, и к тихим багам в бизнес-логике. Правило простое: для строк и дат обычно blank=True, null=False (пустая строка '' предпочтительнее NULL). Для связей ForeignKey и чисел — null=True часто оправдан.
И да, не ленитесь писать __str__. Когда в логах, тестах и админке будет нормальное название вместо Article object (17), вы сэкономите себе часы отладки.
Миграции: история изменений, а не чёрный ящик¶
Миграции — это не магия. Это обычные Python-файлы, где записана дельта между состоянием моделей. makemigrations сравнивает ваши модели с последней зафиксированной миграцией и генерирует код. migrate просто исполняет его по порядку.
Звучит надёжно, пока в команде из трёх разработчиков каждый не создаст свою ветку с 0004_add_field_x.py. И тут начинается веселье: номера полезли вразнобой, зависимости пересекаются, migrate падает с MigrationConflict.
Что делать?
1. Не паниковать. Конфликты миграций — это нормально в командной работе.
2. Смотреть, что именно конфликтует. Чаще всего это просто разный порядок файлов или изменения одних и тех же полей.
3. Переименовывать аккуратно. Django сортирует миграции по префиксу. Если ветки не пересекались по смыслу, можно сдвинуть номера.
4. --merge существует, но используйте его осознанно. Он создаёт пустую миграцию с зависимостями от двух конфликтующих. Работает, но маскирует проблему, если вы меняли одно и то же поле.
Лайфхак: перед мержом ветки запустите python manage.py makemigrations --check. Если упадёт — миграции в ветке рассинхронизированы с main. Чините до пуша.
Когда миграции ломаются (и как с этим жить)¶
Иногда нужно не просто изменить схему, а перенести данные. Для этого есть data-миграции:
def fill_slug(apps, schema_editor):
Article = apps.get_model('blog', 'Article')
for article in Article.objects.all():
article.slug = slugify(article.title) or f'article-{article.pk}'
article.save()
Важный нюанс: в миграциях нельзя импортировать свои модели напрямую. Только через apps.get_model(). Иначе через полгода, когда вы переименуете поле или добавите Meta, старая миграция упадёт с ImportError или AppRegistryNotReady. Миграция должна работать с тем снимком модели, который был актуален в момент её создания.
Ещё одна боль: изменение null=False → null=True (или наоборот) на проде с миллионом строк. Django сгенерирует ALTER TABLE, но на живой базе это может заблокировать таблицу на запись. Тут уже не Django виноват — это особенности PostgreSQL/MySQL.
Правильный подход:
1. Добавить новое поле с null=True.
2. Запустить data-миграцию или фоновый скрипт, который перенесёт значения.
3. Переключить код на новое поле.
4. Удалить старое в отдельной миграции.
Да, дольше. Зато без простоя.
4 правила, которые спасают от боли¶
- Коммитьте миграции. Всегда. Даже если кажется, что это «мусор». Без них ваш проект не воспроизведётся на другом компе, а CI упадёт в первый же день.
- Не бойтесь
squashmigrations, но только на стабильной ветке. Когда файлов накопится 50+, можно сжать их до одного. Но делайте это, когда ветка уже в мейне и никто не тянет старые миграции. - Тестируйте миграции на копии продакшн-дампа. Локально на 10 строках всё летает. На проде с индексами, foreign keys и триггерами — совсем другая история.
- Если миграция упала — не чините её руками в БД. Откатите, исправьте код, запустите заново. Миграции должны быть идемпотентными и воспроизводимыми. Ручные правки в
django_migrationsили схемах — путь к тихому распаду базы.
Вместо заключения¶
Django ORM — один из тех инструментов, которые кажутся «слишком умными», пока не начинаешь понимать, как они работают под капотом. Как только перестаёшь ждать магии и начинаешь читать сгенерированные миграции как обычный код, жизнь становится предсказуемой. А предсказуемость в бэкенде — это уже половина успеха.