Django-Admin.ru

Миграции в Django: makemigrations и migrate

Как работает миграция базы данных в Django: makemigrations и migrate, showmigrations и sqlmigrate, откат, RunPython, --fake, squashmigrations и конфликты.

Редакция Django-Admin.ru
· обновлено · 10 мин чтения

Содержание · 11 разделов
  1. Как устроены миграции
  2. Основные команды
  3. showmigrations: что уже применено
  4. sqlmigrate: какой SQL выполнится
  5. Проверка в CI
  6. Новое обязательное поле: default и прочие варианты
  7. Миграция данных: RunPython
  8. Как откатить миграцию
  9. Фиктивное применение: —fake и —fake-initial
  10. Конфликты миграций в команде
  11. squashmigrations: когда миграций слишком много
  12. Частые ошибки
  13. no such table / relation does not exist
  14. table blog_post has no column named slug
  15. NodeNotFoundError: dependencies reference nonexistent parent node
  16. InconsistentMigrationHistory
  17. Частые вопросы
  18. Как переименовать поле и не потерять данные?
  19. Можно ли удалить все миграции и начать заново?
  20. Как запускать миграции при деплое?
  21. Как применить миграции к другой базе?
  22. Как добавить индекс на большую таблицу PostgreSQL без блокировки записи?
  23. Итог

Миграция базы данных в Django — это два шага: python manage.py makemigrations создаёт файл миграции по изменениям в моделях, а python manage.py migrate применяет его к базе. Файлы миграций лежат в папке migrations каждого приложения и хранятся в git вместе с кодом.

python manage.py makemigrations   # models.py → blog/migrations/000X_*.py
python manage.py migrate          # файлы миграций → таблицы в базе

Ниже — как проверить, что и в каком порядке применится, как откатить миграцию, написать миграцию данных через RunPython, когда нужен --fake и как разрешать конфликты миграций в команде. Примеры проверены на Django 5.2 LTS и 6.x.

Как устроены миграции

Каждая миграция — Python-файл с классом Migration: список зависимостей (dependencies) и операций (operations). Django строит из них граф и знает, в каком порядке их применять. Какие миграции уже выполнены, он записывает в служебную таблицу django_migrations в самой базе.

Возьмём модель из приложения blog (как его создать — в статье про startapp, как описывать поля — в статье «Модели в Django»):

# blog/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField("Заголовок", max_length=200)
    body = models.TextField("Текст")

    def __str__(self):
        return self.title
python manage.py makemigrations blog
Migrations for 'blog':
  blog\migrations\0001_initial.py
    + Create model Post

Сгенерированный файл можно и нужно читать — это обычный код:

# blog/migrations/0001_initial.py
from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=200, verbose_name='Заголовок')),
                ('body', models.TextField(verbose_name='Текст')),
            ],
        ),
    ]

Миграции создаются для любых изменений, которые Django отслеживает: новые и удалённые модели и поля, смена типа, null, unique, индексов, Meta. Даже правка verbose_name или choices даёт миграцию — в базе она ничего не меняет, но держит историю моделей полной.

Основные команды

КомандаЧто делает
makemigrations [app]Создаёт миграции по изменениям в моделях
makemigrations --check --dry-runТолько проверяет, есть ли несозданные миграции
makemigrations app --empty --name имяПустая миграция для своего кода
migrate [app] [миграция]Применяет миграции или откатывает к указанной
migrate --planПоказывает, что будет выполнено, без изменений в базе
showmigrations [app]Список миграций: [X] — применена, [ ] — нет
sqlmigrate app 0002SQL, который выполнит миграция
squashmigrations app 0004Склеивает миграции с первой по указанную

Все команды запускаются из папки с manage.py в активированном виртуальном окружении.

showmigrations: что уже применено

python manage.py showmigrations blog
blog
 [X] 0001_initial
 [X] 0002_post_is_published_post_slug
 [ ] 0003_fill_post_slug

Флаг --plan выводит все миграции проекта в том порядке, в котором Django их применит.

sqlmigrate: какой SQL выполнится

Перед миграцией большой таблицы в продакшне полезно увидеть настоящий SQL:

python manage.py sqlmigrate blog 0001
BEGIN;
--
-- Create model Post
--
CREATE TABLE "blog_post" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(200) NOT NULL, "body" text NOT NULL);
COMMIT;

SQL зависит от базы: на SQLite и PostgreSQL одна и та же миграция выглядит по-разному, поэтому запускайте sqlmigrate с настройками нужной базы. С флагом --backwards команда покажет SQL отката.

Проверка в CI

python manage.py makemigrations --check --dry-run

Команда ничего не записывает и завершается с кодом 1, если модели изменены, а миграция не создана. Добавьте её в CI, и забытая миграция не доедет до продакшна.

Новое обязательное поле: default и прочие варианты

Добавим в модель поле slug без значения по умолчанию. В таблице уже есть строки, и Django не знает, что в них записать:

It is impossible to add a non-nullable field 'slug' to post without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit and manually define a default value in models.py.

Варианты решения:

  • Указать default в модели, если осмысленное значение существует (default=False, default=0).
  • Разрешить пустое значение: null=True или для строк blank=True, default="".
  • Ввести разовое значение (вариант 1) — оно попадёт только в миграцию.
  • Сделать в три шага, если поле должно быть уникальным: добавить с default="", заполнить миграцией данных, затем включить unique=True. Этот путь разобран ниже.

Миграция данных: RunPython

Миграции меняют не только схему, но и данные. Создайте пустую миграцию:

python manage.py makemigrations blog --empty --name fill_post_slug

И впишите в неё функцию:

# blog/migrations/0003_fill_post_slug.py
from django.db import migrations
from django.utils.text import slugify


def fill_slugs(apps, schema_editor):
    Post = apps.get_model("blog", "Post")
    for post in Post.objects.filter(slug=""):
        post.slug = f"{slugify(post.title) or 'post'}-{post.pk}"
        post.save(update_fields=["slug"])


class Migration(migrations.Migration):

    dependencies = [
        ("blog", "0002_post_is_published_post_slug"),
    ]

    operations = [
        migrations.RunPython(fill_slugs, migrations.RunPython.noop),
    ]

Три правила для RunPython:

  1. Берите модель через apps.get_model(), а не импортом из blog.models. Миграция получает «историческую» версию модели — с теми полями, что были на этом шаге. Импорт текущей модели сломается, как только поля изменятся.
  2. Указывайте обратную функцию. Второй аргумент — код отката. Если откатывать нечего, передайте migrations.RunPython.noop. Без него откат упадёт с IrreversibleError: Operation <RunPython ...> in blog.0003_fill_post_slug is not reversible.
  3. Не вызывайте свои методы модели — у исторических моделей нет пользовательских методов, только поля и менеджеры с use_in_migrations = True.

Для slugify учтите: по умолчанию кириллица отбрасывается, и для заголовка «Привет, мир» результат пустой — поэтому в примере есть запасной вариант 'post'. Теперь можно сделать поле уникальным: SlugField(max_length=200, unique=True) и снова makemigrations.

Чистый SQL пишется операцией migrations.RunSQL("...", reverse_sql="...").

Как откатить миграцию

Откат — это тот же migrate, но с именем миграции, до которой нужно вернуться. Всё, что после неё, будет отменено:

python manage.py migrate blog 0002 --plan   # сначала посмотрите план
python manage.py migrate blog 0002
Running migrations:
  Rendering model states... DONE
  Unapplying blog.0004_alter_post_slug... OK
  Unapplying blog.0003_fill_post_slug... OK

Имя можно сокращать до уникального префикса: 0002 вместо 0002_post_is_published_post_slug. Отменить все миграции приложения — python manage.py migrate blog zero. Учтите, что откат удаляет столбцы и таблицы вместе с данными.

Чтобы убрать миграцию совсем, сначала откатите её, затем удалите файл. Если удалить файл применённой миграции, а потом создать новую, история в базе и в коде разойдутся.

Фиктивное применение: —fake и —fake-initial

--fake отмечает миграцию применённой (или отменённой) в django_migrations, не выполняя SQL:

python manage.py migrate blog 0005 --fake

Это нужно, когда схема в базе уже соответствует миграции: изменения внесли вручную или восстановили базу из дампа. Применённая так миграция в выводе помечается FAKED. Ошибка с --fake незаметна сразу и проявляется позже как «нет такого столбца», поэтому сверяйте схему перед запуском.

--fake-initial пропускает начальные миграции, если их таблицы уже существуют — пригодится при подключении Django к готовой базе. Остальные миграции применяются как обычно.

Конфликты миграций в команде

Два разработчика в разных ветках изменили модели одного приложения. Оба получили миграцию 0005, обе зависят от 0004. После слияния веток migrate откажется работать:

CommandError: Conflicting migrations detected; multiple leaf nodes in the migration graph: (0005_post_author_name, 0005_post_views in blog).
To fix them run 'python manage.py makemigrations --merge'

Если изменения не пересекаются (разные поля), создайте миграцию слияния:

python manage.py makemigrations --merge

Django создаст 0006_merge_... с двумя зависимостями и пустым списком операций. Если обе ветки меняли одно и то же поле, надёжнее откатить свою миграцию локально, удалить её, подтянуть чужую и заново выполнить makemigrations.

Чтобы конфликтов было меньше:

  • коммитьте миграцию вместе с изменением модели, в одном коммите;
  • не редактируйте миграции, которые уже применены на сервере или у коллег;
  • запускайте makemigrations --check в CI.

squashmigrations: когда миграций слишком много

За годы в приложении копятся сотни миграций, и тесты создают базу всё дольше. Команда склеивает цепочку в одну:

python manage.py squashmigrations blog 0004

Django создаст 0001_squashed_0004_alter_post_slug.py с атрибутом replaces. Старые файлы удалять сразу нельзя: новая миграция используется для свежих установок, а базы, где старые уже применены, продолжают работать. Удаляют их, когда все окружения обновлены. Если среди склеиваемых есть RunPython, Django предупредит Manual porting required — функции нужно перенести в новый файл вручную.

Частые ошибки

no such table / relation does not exist

Миграции не применены — выполните python manage.py migrate. На SQLite ошибка выглядит как OperationalError: no such table: blog_post, на PostgreSQL — relation "blog_post" does not exist. Если migrate пишет No migrations to apply, проверьте, что приложение есть в INSTALLED_APPS и у него есть папка migrations с __init__.py.

table blog_post has no column named slug

Поле добавлено в модель, но миграция не создана или не применена. Предупреждение об этом migrate выводит сам:

Your models in app(s): 'blog' have changes that are not yet reflected in a migration, and so won't be applied.

Выполните makemigrations, затем migrate.

NodeNotFoundError: dependencies reference nonexistent parent node

Миграция ссылается на файл, которого нет: его удалили или не закоммитили. Верните файл из git или поправьте dependencies.

InconsistentMigrationHistory

Сообщение вида Migration admin.0001_initial is applied before its dependency accounts.0001_initial чаще всего появляется, когда свою модель пользователя (AUTH_USER_MODEL) добавили после первого migrate. На учебном проекте проще пересоздать базу; на рабочем это отдельная миграция данных. Поэтому свою модель пользователя задают до первой миграции.

Частые вопросы

Как переименовать поле и не потерять данные?

Переименуйте поле в модели и запустите makemigrations. Если тип и параметры поля не изменились, Django спросит:

Was post.title renamed to post.name (a CharField)? [y/N]

Ответ y создаст операцию RenameField, которая переименует столбец и сохранит данные. Ответ N даст удаление старого поля и добавление нового — данные пропадут. Если вы одновременно меняете имя и тип, сделайте это двумя миграциями. Для моделей есть аналогичный вопрос и операция RenameModel.

Можно ли удалить все миграции и начать заново?

На учебном проекте — да: удалите базу и файлы в папках migrations, кроме __init__.py, затем выполните makemigrations и migrate. На рабочей базе так делать нельзя: таблица django_migrations перестанет совпадать с файлами. Там длинную историю сокращают через squashmigrations.

Как запускать миграции при деплое?

Отдельным шагом перед запуском новой версии приложения, без интерактивных вопросов:

python manage.py migrate --noinput

Не запускайте migrate одновременно из нескольких экземпляров приложения: при масштабировании это отдельная задача деплоя или init-контейнер. Пример для контейнеров — в статье про Django в Docker.

Как применить миграции к другой базе?

Если в DATABASES несколько баз, укажите алиас флагом --database:

python manage.py migrate --database=analytics

Какие модели попадут в эту базу, решает роутер баз данных (DATABASE_ROUTERS) через метод allow_migrate().

Как добавить индекс на большую таблицу PostgreSQL без блокировки записи?

Обычный CREATE INDEX блокирует запись в таблицу до конца построения. В PostgreSQL добавьте индекс в Meta.indexes, выполните makemigrations, а в созданной миграции замените AddIndex на AddIndexConcurrently и отключите транзакцию — CREATE INDEX CONCURRENTLY внутри транзакции не работает:

from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models


class Migration(migrations.Migration):
    atomic = False

    dependencies = [("blog", "0004_alter_post_slug")]

    operations = [
        AddIndexConcurrently(
            "post",
            models.Index(fields=["title"], name="blog_post_title_idx"),
        ),
    ]

Итог

Изменили модель — выполните makemigrations, прочитайте созданный файл, проверьте SQL через sqlmigrate и примените migrate. Откат — migrate app <номер>, данные переносятся через RunPython с обратной функцией, а конфликты в команде решает makemigrations --merge. Как работать с данными после миграции, смотрите в шпаргалке по Django ORM. Переезд на боевую СУБД — в статье «Как подключить PostgreSQL к Django», а запуск миграций в контейнере — в статье про Django в Docker.

  • #Django
  • #Миграции
  • #makemigrations
  • #migrate
  • #Django ORM
↑ ↓ выборEnter открытьEsc закрыть