Написание миграции базы данных

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

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

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

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

  • В первом пустом файле миграции добавьте операцию 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Соответственно измените второй аргумент операции.

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

Если вы измените a 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могут не применяться.

Copyright ©2021 All rights reserved