Django ORM

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

Миграции Django ORM | Курс Django ORM урок 1.4
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 6

Цель урока

Разобраться, как 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

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