Миграции в Django: makemigrations и migrate
Как работает миграция базы данных в Django: makemigrations и migrate, showmigrations и sqlmigrate, откат, RunPython, --fake, squashmigrations и конфликты.
Содержание · 11 разделов
- Как устроены миграции
- Основные команды
- showmigrations: что уже применено
- sqlmigrate: какой SQL выполнится
- Проверка в CI
- Новое обязательное поле: default и прочие варианты
- Миграция данных: RunPython
- Как откатить миграцию
- Фиктивное применение: —fake и —fake-initial
- Конфликты миграций в команде
- squashmigrations: когда миграций слишком много
- Частые ошибки
- no such table / relation does not exist
- table blog_post has no column named slug
- NodeNotFoundError: dependencies reference nonexistent parent node
- InconsistentMigrationHistory
- Частые вопросы
- Как переименовать поле и не потерять данные?
- Можно ли удалить все миграции и начать заново?
- Как запускать миграции при деплое?
- Как применить миграции к другой базе?
- Как добавить индекс на большую таблицу PostgreSQL без блокировки записи?
- Итог
Миграция базы данных в 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 0002 | SQL, который выполнит миграция |
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:
- Берите модель через
apps.get_model(), а не импортом изblog.models. Миграция получает «историческую» версию модели — с теми полями, что были на этом шаге. Импорт текущей модели сломается, как только поля изменятся. - Указывайте обратную функцию. Второй аргумент — код отката. Если откатывать нечего, передайте
migrations.RunPython.noop. Без него откат упадёт сIrreversibleError: Operation <RunPython ...> in blog.0003_fill_post_slug is not reversible. - Не вызывайте свои методы модели — у исторических моделей нет пользовательских методов, только поля и менеджеры с
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