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=Falsenull=True (или наоборот) на проде с миллионом строк. Django сгенерирует ALTER TABLE, но на живой базе это может заблокировать таблицу на запись. Тут уже не Django виноват — это особенности PostgreSQL/MySQL.
Правильный подход:
1. Добавить новое поле с null=True.
2. Запустить data-миграцию или фоновый скрипт, который перенесёт значения.
3. Переключить код на новое поле.
4. Удалить старое в отдельной миграции.

Да, дольше. Зато без простоя.


4 правила, которые спасают от боли

  1. Коммитьте миграции. Всегда. Даже если кажется, что это «мусор». Без них ваш проект не воспроизведётся на другом компе, а CI упадёт в первый же день.
  2. Не бойтесь squashmigrations, но только на стабильной ветке. Когда файлов накопится 50+, можно сжать их до одного. Но делайте это, когда ветка уже в мейне и никто не тянет старые миграции.
  3. Тестируйте миграции на копии продакшн-дампа. Локально на 10 строках всё летает. На проде с индексами, foreign keys и триггерами — совсем другая история.
  4. Если миграция упала — не чините её руками в БД. Откатите, исправьте код, запустите заново. Миграции должны быть идемпотентными и воспроизводимыми. Ручные правки в django_migrations или схемах — путь к тихому распаду базы.

Вместо заключения

Django ORM — один из тех инструментов, которые кажутся «слишком умными», пока не начинаешь понимать, как они работают под капотом. Как только перестаёшь ждать магии и начинаешь читать сгенерированные миграции как обычный код, жизнь становится предсказуемой. А предсказуемость в бэкенде — это уже половина успеха.

← Все публикации