Spec-Zone.ru › Django 6.0

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

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

Миграции данных и несколько баз данных

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

Для этого можно проверить псевдоним подключения к базе данных внутри операции RunPython, обратившись к атрибуту schema_editor.connection.alias:

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),
    ]

Также можно передать подсказки, которые будут переданы методу allow_migrate() маршрутизаторов баз данных в качестве **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 в качестве подсказки, чтобы сделать её максимально понятной для маршрутизатора. Это особенно важно для повторно используемых и сторонних приложений.

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

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

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

  • Добавьте поле в модель с аргументами 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.

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

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

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

from django.db import migrations


class Migration(migrations.Migration):
    atomic = False

В такой миграции все операции выполняются без транзакции. Части миграции можно выполнить внутри транзакции, используя atomic() или передав atomic=True в 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). (Поддержка атомарных операторов DDL в MySQL относится к отдельным операторам, а не к нескольким операторам, заключённым в транзакцию, которую можно откатить.)

Управление порядком миграций

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 в миграции, которую нужно выполнить после создаваемой вами, нежелательно или непрактично.

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

Для переноса данных из одного стороннего приложения в другое можно использовать миграцию данных.

Если вы планируете позже удалить старое приложение, необходимо задать свойство 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

Если изменить ManyToManyField, указав для него модель through, миграция по умолчанию удалит существующую таблицу и создаст новую, в результате чего существующие связи будут потеряны. Чтобы этого избежать, можно использовать SeparateDatabaseAndState, чтобы переименовать существующую таблицу в новое имя, одновременно сообщив автоопределителю миграций, что новая модель создана. Имя существующей таблицы можно проверить с помощью sqlmigrate или dbshell. Новое имя таблицы можно проверить по свойству _meta.db_table промежуточной модели. В вашей новой модели through должны использоваться те же имена для ForeignKey, что и в Django. Если нужны дополнительные поля, их также следует добавить в операциях после 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",
                            ),
                        ),
                    ],
                ),
                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, могут не примениться.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/howto/writing-migrations/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API