How to create database migrations

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

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

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

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

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

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

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

  • В первый пустой файл миграции добавьте операцию 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). (В MySQL атрибут atomic DDL statement support относится к отдельным утверждениям, а не к нескольким утверждениям, завернутым в транзакцию, которая может быть откатана).

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

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

Вернуться на верх