Django ORM

Миграции Django ORM | Курс Django ORM урок 1.4

Миграции Django ORM | Курс Django ORM урок 1.4
Михаил Омельченко
Автор
Михаил Омельченко
Опубликовано 16.03.2026
0,0
Views 168

Цель урока

Разобраться, как Django отслеживает изменения схемы БД, что происходит внутри файла миграции, как безопасно откатываться и как писать миграции для изменения данных (data migrations). После этого урока миграции перестанут быть черным ящиком.

Необходимые знания

  • Уроки 1.1-1.3: модели, поля, Meta
  • Базовое понимание DDL (CREATE TABLE, ALTER TABLE)

Как Django отслеживает схему

Django хранит историю миграций в двух местах:

  1. Файлы миграций в директории migrations/ каждого приложения, это код Python описывающий изменения
  2. Таблица 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:

  1. Django видит, что текущее состояние - 0002
  2. Цель - 0001
  3. Выполняет database_backwards для 0002
  4. Обновляет django_migrations, удаляет запись о 0002

Что нельзя откатить автоматически:

  • RunPython без явного reverse_code
  • RunSQL без явного reverse_sql
  • RemoveField, данные удалены, колонки нет
# 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, которая содержит итоговое состояние схемы. Это ускоряет запуск тестов (где БД создается с нуля) и упрощает историю.


Практическое задание

  1. Откройте любой файл миграции из shop/migrations/ и найдите в нём dependencies и operations. Запустите sqlmigrate для этой миграции и сравните SQL с DDL в psql через \d.

  2. Добавьте в модель Product поле weight = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True). Создайте и примените миграцию. Затем сделай его обязательным (null=False, db_default=0). Создайте и примените вторую миграцию.

  3. Создайте data migration, которая заполняет weight значением 0.5 для всех существующих продуктов категории "books". Используйте apps.get_model.

  4. Откати последнюю миграцию через 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 начинаем работу с данными: разберем все способы создания объектов, их отличия в количестве запросов и когда какой метод уместен.


<< Урок 1.3

Урок 2.1 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы