Spec-Zone.ru › Django 5.1

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

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

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

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

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

Поэтому необходимо выполнить следующие действия. В этом примере мы добавим поле без значения NULL 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.

    Обратите внимание, что существует проблема гонки, если вы разрешаете создание объектов во время выполнения этой миграции. Объекты, созданные после 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/5.1/howto/writing-migrations/

Spec-Zone.ru

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