• 3.1
  • 3.2
  • 5.0
  • Версия документации: 6.1

Как создавать миграции базы данных

В этом документе объясняется, как писать миграции базы данных для различных сценариев, с которыми вы можете столкнуться. Вводные материалы по миграциям см. в руководстве по теме.

Миграции данных и множественные базы данных

При использовании нескольких баз данных вам может потребоваться решить, следует ли запускать миграцию для определенной базы данных. Например, вы можете захотеть запустить миграцию только для определенной базы данных.

Чтобы сделать это, вы можете проверить соединение с базой данных внутри операции RunPython, посмотрев на schema_editor.connection.alias attribute:

from django.db import migrations


def forwards(apps, schema_editor):
    if schema_editor.connection.alias != "default":
        return
    # Your migration code goes here


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards),
    ]

You can also provide hints that will be passed to the allow_migrate() method of database routers as **hints:

myapp/dbrouters.py
class MyRouter:
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if "target_db" in hints:
            return db == hints["target_db"]
        return True

Затем, чтобы использовать это в своих миграциях, выполните следующие действия:

from django.db import migrations


def forwards(apps, schema_editor):
    # Your migration code goes here
    ...


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards, hints={"target_db": "default"}),
    ]

Если ваша операция RunPython или RunSQL влияет только на одну модель, хорошей практикой будет передать model_name в качестве подсказки, чтобы сделать ее максимально прозрачной для маршрутизатора. Это особенно важно для повторно используемых и сторонних приложений.

Миграции, добавляющие уникальные поля

Применение «простой» миграции, которая добавляет уникальное ненулевое поле в таблицу с существующими строками, вызовет ошибку, поскольку значение, используемое для заполнения существующих строк, генерируется только один раз, тем самым нарушая ограничение уникальности.

Поэтому необходимо выполнить следующие шаги. В этом примере мы добавим UUIDField со значением по умолчанию. Измените соответствующее поле в соответствии с вашими потребностями.

  • Добавьте поле в вашу модель с аргументами default=uuid.uuid4 и unique=True (выберите подходящее значение по умолчанию для типа добавляемого поля).

  • Запустите команду makemigrations. Это должно сгенерировать миграцию с операцией AddField.

  • Создайте два пустых файла миграции для одного и того же приложения, дважды запустив makemigrations myapp --empty. Мы переименовали файлы миграции, чтобы дать им осмысленные имена в примерах ниже.

  • Скопируйте операцию AddField из автоматически сгенерированной миграции (первый из трех новых файлов) в последнюю миграцию, измените AddField на AlterField и добавьте импорты uuid и models. Например:

    0006_remove_uuid_null.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations, models
    import uuid
    
    
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0005_populate_uuid_values"),
        ]
    
        operations = [
            migrations.AlterField(
                model_name="mymodel",
                name="uuid",
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    
  • Отредактируйте первый файл миграции. Сгенерированный класс миграции должен выглядеть примерно так:

    0004_add_uuid_field.py
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0003_auto_20150129_1705"),
        ]
    
        operations = [
            migrations.AddField(
                model_name="mymodel",
                name="uuid",
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    

    Измените unique=True на null=True — это создаст промежуточное поле null и отложит создание уникального ограничения до тех пор, пока мы не заполним уникальные значения во всех строках.

  • В первом пустом файле миграции добавьте операцию RunPython или RunSQL для генерации уникального значения (UUID в примере) для каждой существующей строки. Также добавьте импорт uuid. Например:

    0005_populate_uuid_values.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations
    import uuid
    
    
    def gen_uuid(apps, schema_editor):
        MyModel = apps.get_model("myapp", "MyModel")
        for row in MyModel.objects.all():
            row.uuid = uuid.uuid4()
            row.save(update_fields=["uuid"])
    
    
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0004_add_uuid_field"),
        ]
    
        operations = [
            # omit reverse_code=... if you don't want the migration to be reversible.
            migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
        ]
    
  • Теперь вы можете применить миграции как обычно с помощью команды migrate

    Обратите внимание, что если вы разрешите создание объектов во время выполнения этой миграции, возникнет состояние гонки (race condition). У объектов, созданных после AddField и до RunPython, будут перезаписаны их исходные uuid.

Неатомарные миграции

В базах данных, поддерживающих транзакции DDL (SQLite и PostgreSQL), миграции будут выполняться внутри транзакции по умолчанию. Для таких случаев, как выполнение миграций данных в больших таблицах, вы можете захотеть предотвратить выполнение миграции в транзакции, установив атрибут atomic в False:

from django.db import migrations


class Migration(migrations.Migration):
    atomic = False

Within such a migration, all operations are run without a transaction. It’s possible to execute parts of the migration inside a transaction using atomic() or by passing atomic=True to RunPython.

Вот пример неатомарной миграции данных, которая обновляет большую таблицу меньшими партиями:

import uuid

from django.db import migrations, transaction


def gen_uuid(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    while MyModel.objects.filter(uuid__isnull=True).exists():
        with transaction.atomic():
            for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
                row.uuid = uuid.uuid4()
                row.save()


class Migration(migrations.Migration):
    atomic = False

    operations = [
        migrations.RunPython(gen_uuid),
    ]

Атрибут atomic не влияет на базы данных, которые не поддерживают транзакции DDL (например, MySQL, Oracle). (Поддержка операторов ``atomic DDL““ MySQL <https://dev.mysql.com/doc/refman/en/atomic-ddl.html>`_ относится к отдельным операторам, а не к нескольким операторам, заключенным в транзакцию, которую можно откатить.)

Контроль порядка миграций

Django определяет порядок, в котором должны применяться миграции, не по имени файла каждой миграции, а путем построения графика с использованием двух свойств класса Migration: dependencies и run_before.

Если вы использовали команду makemigrations, вы, вероятно, уже видели dependencies в действии.

Свойство dependencies объявляется следующим образом:

from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
    ]

Обычно этого достаточно, но время от времени вам может потребоваться убедиться, что ваша миграция выполняется до других миграций. Это полезно, например, для того, чтобы миграции сторонних приложений выполнялись после замены AUTH_USER_MODEL.

Чтобы добиться этого, поместите все миграции, которые должны зависеть от вашей, в атрибут run_before вашего класса Migration:

class Migration(migrations.Migration):
    ...

    run_before = [
        ("third_party_app", "0001_do_awesome"),
    ]

По возможности используйте dependencies вместо run_before. run_before следует использовать только в том случае, если нежелательно или непрактично указывать dependencies в миграции, которую вы хотите запустить после той, которую вы пишете.

Миграция данных между сторонних приложениях

You can use a data migration to move data from one third-party application to another.

Если вы планируете удалить старое приложение позже, вам нужно будет задать свойство dependencies в зависимости от того, установлено ли старое приложение. В противном случае у вас будут отсутствующие зависимости после удаления старого приложения. Аналогично вам нужно будет перехватить LookupError в apps.вызов get_model(), который извлекает модели из старого приложения. Такой подход позволяет вам развернуть свой проект в любом месте без предварительной установки и последующего удаления старого приложения.

Вот пример миграции:

myapp/migrations/0124_move_old_app_to_new_app.py
from django.apps import apps as global_apps
from django.db import migrations


def forwards(apps, schema_editor):
    try:
        OldModel = apps.get_model("old_app", "OldModel")
    except LookupError:
        # The old app isn't installed.
        return

    NewModel = apps.get_model("new_app", "NewModel")
    NewModel.objects.bulk_create(
        NewModel(new_attribute=old_object.old_attribute)
        for old_object in OldModel.objects.all()
    )


class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(forwards, migrations.RunPython.noop),
    ]
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
        ("new_app", "0001_initial"),
    ]

    if global_apps.is_installed("old_app"):
        dependencies.append(("old_app", "0001_initial"))

Также подумайте, что вы хотите сделать, когда миграция не применяется. Вы можете либо ничего не делать (как в примере выше), либо удалить некоторые или все данные из нового приложения. Измените второй аргумент операции RunPython соответствующим образом.

Изменение ManyToManyField для использования through модели

If you change a ManyToManyField to use a through model, the default migration will delete the existing table and create a new one, losing the existing relations. To avoid this, you can use SeparateDatabaseAndState to rename the existing table to the new table name while telling the migration autodetector that the new model has been created. You can check the existing table name and constraint name through sqlmigrate or dbshell. You can check the new table name with the through model’s _meta.db_table property. Your new through model should use the same names for the ForeignKeys as Django did. Also if it needs any extra fields, they should be added in operations after SeparateDatabaseAndState.

Например, если бы у нас была модель Book с ManyToManyField, связывающая с Author, мы могли бы добавить сквозную модель AuthorBook с новым полем is_primary, например так:

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
    dependencies = [
        ("core", "0001_initial"),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                # Old table name from checking with sqlmigrate, new table
                # name from AuthorBook._meta.db_table.
                migrations.RunSQL(
                    sql="ALTER TABLE core_book_authors RENAME TO core_authorbook",
                    reverse_sql="ALTER TABLE core_authorbook RENAME TO core_book_authors",
                ),
            ],
            state_operations=[
                migrations.CreateModel(
                    name="AuthorBook",
                    fields=[
                        (
                            "id",
                            models.AutoField(
                                auto_created=True,
                                primary_key=True,
                                serialize=False,
                                verbose_name="ID",
                            ),
                        ),
                        (
                            "author",
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to="core.Author",
                            ),
                        ),
                        (
                            "book",
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to="core.Book",
                            ),
                        ),
                    ],
                    options={
                        "constraints": [
                            models.UniqueConstraint(
                                fields=["author", "book"],
                                name="unique_author_book",
                            )
                        ],
                    },
                ),
                migrations.AlterField(
                    model_name="book",
                    name="authors",
                    field=models.ManyToManyField(
                        to="core.Author",
                        through="core.AuthorBook",
                    ),
                ),
            ],
        ),
        migrations.AddField(
            model_name="authorbook",
            name="is_primary",
            field=models.BooleanField(default=False),
        ),
    ]

Изменение неуправляемой модели на управляемую

Если вы хотите изменить неуправляемую модель (managed=False) на управляемую, необходимо удалить managed=False и создать миграцию перед внесением других изменений, связанных со схемой, в модель, поскольку изменения схемы, которые появляются в миграции, содержащей операцию по изменению Meta.managed, могут быть не применены.

Back to Top