Миграции Django ORM | Курс Django ORM урок 1.4
Цель урока
Разобраться, как Django отслеживает изменения схемы БД, что происходит внутри файла миграции, как безопасно откатываться и как писать миграции для изменения данных (data migrations). После этого урока миграции перестанут быть черным ящиком.
Необходимые знания
- Уроки 1.1-1.3: модели, поля, Meta
- Базовое понимание DDL (CREATE TABLE, ALTER TABLE)
Как Django отслеживает схему
Django хранит историю миграций в двух местах:
- Файлы миграций в директории
migrations/каждого приложения, это код Python описывающий изменения - Таблица
django_migrationsв БД, журнал применённых миграций
docker compose exec db psql -U postgres -d ormcourse -c "SELECT * FROM django_migrations WHERE app = 'shop';"
id | app | name | applied
----+------+-------------------+-------------------------------
19 | shop | 0001_initial | 2026-03-06 12:39:26.708514+00
20 | shop | 0002_tag_product_tags | 2026-03-06 16:47:28.249+00
Когда вы запускаете migrate, Django сравнивает файлы миграций с этой таблицей и применяет только те, которых там ещё нет.
makemigrations не трогает БД, он только создает файлы, сравнивая текущее состояние моделей с состоянием зафиксированным в предыдущих миграциях.
Анатомия файла миграции
Файл shop/migrations/0001_initial.py:
# Generated by Django 6.0.3 on 2026-03-06 12:39
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
# Список миграций, которые должны быть применены раньше этой
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# Список операций, что именно делать с БД
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('slug', models.SlugField(max_length=200, unique=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='shop.category')),
],
options={
'verbose_name_plural': 'categories',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Order',
fields=[
# ... поля ...
],
),
# ... остальные модели ...
]
Файл миграции, это класс Migration с двумя атрибутами:
dependencies, граф зависимостей. Эта миграция применяется только после указанных. Именно так Django строит правильный порядок применения.operations, список операций над схемой. Каждая операция знает, как применить изменениеdatabase_forwardsи как откатить егоdatabase_backwards.
Что умеют operations
migrations.CreateModel(...) # CREATE TABLE
migrations.DeleteModel(...) # DROP TABLE
migrations.AddField(...) # ALTER TABLE ... ADD COLUMN
migrations.RemoveField(...) # ALTER TABLE ... DROP COLUMN
migrations.AlterField(...) # ALTER TABLE ... ALTER COLUMN
migrations.RenameField(...) # ALTER TABLE ... RENAME COLUMN
migrations.AddIndex(...) # CREATE INDEX
migrations.RemoveIndex(...) # DROP INDEX
migrations.AddConstraint(...) # ALTER TABLE ... ADD CONSTRAINT
migrations.RunSQL(...) # произвольный SQL
migrations.RunPython(...) # произвольный код Python
Основные команды
# Создать файлы миграций по изменениям в models.py
python manage.py makemigrations
# Создать миграцию для конкретного приложения
python manage.py makemigrations shop
# Показать SQL, который будет выполнен (без применения)
python manage.py sqlmigrate shop 0001
# Применить все непримененные миграции
python manage.py migrate
# Применить миграции конкретного приложения
python manage.py migrate shop
# Показать статус всех миграций
python manage.py showmigrations shop
Вывод showmigrations:
shop
[X] 0001_initial # применена
[X] 0002_product_tags # применена
[ ] 0003_add_discount # ещё не применена
sqlmigrate позволяет увидеть SQL перед применением:
docker compose exec web python manage.py sqlmigrate shop 0001
BEGIN;
CREATE TABLE "shop_category" (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"name" varchar(200) NOT NULL,
"slug" varchar(200) NOT NULL UNIQUE,
"parent_id" bigint NULL
);
CREATE INDEX "shop_category_slug_4508178e_like" ON "shop_category" ("slug" varchar_pattern_ops);
-- ...
COMMIT;
Откат миграций
Django умеет откатывать миграции, если операция поддерживает обратное действие. CreateModel откатывается через DROP TABLE, AddField через DROP COLUMN.
# Откатиться к конкретной миграции (применить обратные операции всех позже неё)
python manage.py migrate shop 0001
# Откатить все миграции приложения
python manage.py migrate shop zero
Что происходит при откате 0002 после применения 0001 и 0002:
- Django видит, что текущее состояние -
0002 - Цель -
0001 - Выполняет
database_backwardsдля0002 - Обновляет
django_migrations, удаляет запись о0002
Что нельзя откатить автоматически:
RunPythonбез явногоreverse_codeRunSQLбез явногоreverse_sqlRemoveField, данные удалены, колонки нет
# RunPython с поддержкой отката
migrations.RunPython(
code=populate_slugs,
reverse_code=migrations.RunPython.noop, # откат ничего не делать
)
Изменение схемы БД
Добавление новой NOT NULL колонки в таблицу с данными, это проблема.
# Миграция упадет если в shop_product уже есть строки
class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name="product",
name="weight",
field=models.DecimalField(max_digits=6, decimal_places=2),
),
]
django.db.utils.IntegrityError: column "weight" of relation "shop_product"
contains null values
PostgreSQL не может добавить NOT NULL колонку без значения для существующих строк.
Решение 1: db_default
field=models.DecimalField(max_digits=6, decimal_places=2, db_default=0),
PostgreSQL заполнит существующие строки значением 0 прямо в DDL. Самый чистый вариант.
Решение 2: два шага
# Шаг 1: добавить как nullable
migrations.AddField(
model_name="product",
name="weight",
field=models.DecimalField(max_digits=6, decimal_places=2, null=True),
),
# Шаг 2: отдельная миграция после заполнения данных
migrations.AlterField(
model_name="product",
name="weight",
field=models.DecimalField(max_digits=6, decimal_places=2),
),
На больших таблицах ALTER COLUMN SET NOT NULL блокирует таблицу на время проверки. PostgreSQL 12+ умеет делать это без блокировки через NOT VALID constraint, но это уже тема для продвинутой работы с миграциями в production.
Data migrations - миграции данных
RunPython позволяет выполнять произвольный код Python внутри миграции. Это нужно для изменения данных при изменении схемы.
Пример, добавляем поле slug в Category, которого раньше не было. Нужно заполнить его для существующих записей.
python manage.py makemigrations shop --empty --name=populate_category_slugs
Создается пустая миграция:
# shop/migrations/0003_populate_category_slugs.py
from django.db import migrations
from django.utils.text import slugify
def populate_slugs(apps, schema_editor):
# Важно: используем apps.get_model, а НЕ прямой импорт модели
# apps.get_model возвращает версию модели на момент этой миграции
Category = apps.get_model("shop", "Category")
for category in Category.objects.all():
category.slug = slugify(category.name)
category.save()
def reverse_populate_slugs(apps, schema_editor):
Category = apps.get_model("shop", "Category")
Category.objects.all().update(slug="")
class Migration(migrations.Migration):
dependencies = [
("shop", "0002_category_add_slug"),
]
operations = [
migrations.RunPython(
code=populate_slugs,
reverse_code=reverse_populate_slugs,
),
]
Почему apps.get_model, а не прямой импорт
Это ключевой момент data migrations:
# НЕПРАВИЛЬНО, никогда так не делайте в миграциях
from shop.models import Category # импортирует ТЕКУЩУЮ версию модели
# ПРАВИЛЬНО
Category = apps.get_model("shop", "Category") # версия модели на момент миграции
Если вы импортируете модель напрямую и через год добавите в неё поле, при повторном запуске миграции с нуля (например, на новой БД) код миграции будет использовать модель с новым полем, которого в той схеме ещё нет и миграция упадет.
apps.get_model возвращает "замороженную" версию модели, такой, какой она была в момент написания миграции.
Производительность data migrations
save() в цикле будет медленный для больших таблиц. Используйте update() или bulk_update():
def populate_slugs(apps, schema_editor):
Category = apps.get_model("shop", "Category")
# Плохо для больших таблиц, N запросов
for category in Category.objects.all():
category.slug = slugify(category.name)
category.save()
# Лучше если значение одинаковое для всех
Category.objects.filter(slug="").update(slug="default")
Для сложных вычислений, где значение зависит от данных каждой строки, save() в цикле неизбежен, но можно использовать iterator() и bulk_update() для снижения нагрузки на память:
def populate_slugs(apps, schema_editor):
Category = apps.get_model("shop", "Category")
batch = []
for category in Category.objects.iterator(chunk_size=500):
category.slug = slugify(category.name)
batch.append(category)
if len(batch) >= 500:
Category.objects.bulk_update(batch, ["slug"])
batch = []
if batch:
Category.objects.bulk_update(batch, ["slug"])
Конфликты миграций
Если два разработчика одновременно создали миграции одной базы:
shop/migrations/
0002_add_tags.py # создал разработчик A
0002_add_discount.py # создал разработчик B
Обе ссылаются на 0001 как на зависимость. При попытке мигрировать Django видит конфликт:
CommandError: Conflicting migrations detected; multiple leaf nodes in the
migration graph: (0002_add_tags, 0002_add_discount).
Решение:
python manage.py makemigrations --merge shop
Django создает миграцию 0003_merge_... с зависимостями от обоих 0002:
class Migration(migrations.Migration):
dependencies = [
("shop", "0002_add_tags"),
("shop", "0002_add_discount"),
]
operations = [] # пусто, просто объединяет ветки
После этого граф миграций снова линейный.
squashmigrations - сжатие истории
Со временем в приложении накапливаются десятки миграций. squashmigrations объединяет их в одну:
python manage.py squashmigrations shop 0001 0010
Создает 0001_squashed_0010.py, которая содержит итоговое состояние схемы. Это ускоряет запуск тестов (где БД создается с нуля) и упрощает историю.
Практическое задание
-
Откройте любой файл миграции из
shop/migrations/и найдите в нёмdependenciesиoperations. Запуститеsqlmigrateдля этой миграции и сравните SQL с DDL в psql через\d. -
Добавьте в модель
Productполеweight = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True). Создайте и примените миграцию. Затем сделай его обязательным (null=False,db_default=0). Создайте и примените вторую миграцию. -
Создайте data migration, которая заполняет
weightзначением0.5для всех существующих продуктов категории "books". Используйтеapps.get_model. -
Откати последнюю миграцию через
migrate shop <предыдущий_номер>. Убедитесь что откат прошел используяshowmigrations.
Возможные ошибки
Прямой импорт модели в data migration
# Ломается при повторном применении в будущем
from shop.models import Category
Всегда используйте apps.get_model("shop", "Category").
Не задать reverse_code для RunPython
# Миграция применяется, но не откатывается
migrations.RunPython(populate_slugs)
# Правильно - явно указать что делать при откате
migrations.RunPython(populate_slugs, reverse_code=migrations.RunPython.noop)
Добавить NOT NULL поле без db_default на таблицу с данными
Миграция упадет в production. Всегда либо используйте db_default, либо делай два шага: сначала nullable, потом не nullable после заполнения.
Ручное редактирование таблицы django_migrations
Не делайте этого. Если нужно "притвориться", что миграция применена без выполнения:
python manage.py migrate shop 0003 --fake
--fake обновляет django_migrations без выполнения SQL. Используется при переносе схемы вручную или при squash.
Связь со следующим уроком
Блок 1 завершен, у нас есть полное понимание того, как Django описывает схему БД через модели и синхронизирует её через миграции. В уроке 2.1 начинаем работу с данными: разберем все способы создания объектов, их отличия в количестве запросов и когда какой метод уместен.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru