编写数据库迁移 — Django 文档

来自菜鸟教程
Django/docs/3.1.x/howto/writing-migrations
跳转至:导航、​搜索

编写数据库迁移

本文档解释了如何为您可能遇到的不同场景构建和编写数据库迁移。 有关迁移的介绍性材料,请参阅 主题指南

数据迁移和多个数据库

使用多个数据库时,您可能需要确定是否针对特定数据库运行迁移。 例如,您可能希望 在特定数据库上运行迁移。

为此,您可以通过查看 schema_editor.connection.alias 属性来检查 RunPython 操作中的数据库连接别名:

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

您还可以提供将作为 **hints 传递给数据库路由器的 allow_migrate() 方法的提示:

我的应用程序/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'}),
    ]

如果您的 RunPythonRunSQL 操作仅影响一个模型,最好将 model_name 作为提示传递给路由器,使其尽可能透明。 这对于可重用和第三方应用程序尤其重要。


添加唯一字段的迁移

应用向具有现有行的表添加唯一不可为空字段的“普通”迁移将引发错误,因为用于填充现有行的值仅生成一次,从而破坏唯一约束。

因此,应采取以下步骤。 在这个例子中,我们将添加一个不可为空的 UUIDField 和一个默认值。 根据您的需要修改相应的字段。

  • 使用 default=uuid.uuid4unique=True 参数在模型上添加字段(为您添加的字段类型选择适当的默认值)。

  • 运行 :djadmin:`makemigrations` 命令。 这应该会生成一个带有 AddField 操作的迁移。

  • 通过运行 makemigrations myapp --empty 两次,为同一个应用程序生成两个空迁移文件。 我们已重命名迁移文件,以便在下面的示例中为它们提供有意义的名称。

  • AddField操作从自动生成的迁移(三个新文件中的第一个)复制到最后一个迁移,将AddField改为AlterField,并添加uuidmodels。 例如:

    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 – 这将创建中间空字段并推迟创建唯一约束,直到我们在所有行上填充了唯一值。

  • 在第一个空迁移文件中,添加 RunPythonRunSQL 操作,为每个现有行生成唯一值(示例中为 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),
        ]
  • 现在您可以像往常一样使用 :djadmin:`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、甲骨文)。 (MySQL 的 原子 DDL 语句支持 是指单个语句,而不是包含在可以回滚的事务中的多个语句。)


控制迁移顺序

Django 不是通过每个迁移的文件名来确定迁移应用的顺序,而是通过使用 Migration 类上的两个属性构建一个图:dependenciesrun_before

如果您使用过 :djadmin:`makemigrations` 命令,您可能已经看到了 dependencies 的作用,因为自动创建的迁移已将其定义为创建过程的一部分。

dependencies 属性声明如下:

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0123_the_previous_migration'),
    ]

通常这就足够了,但有时您可能需要确保您的迁移在 其他迁移之前运行 。 这很有用,例如,使第三方应用程序的迁移在 之后运行 您的 :setting:`AUTH_USER_MODEL` 替换。

要实现这一点,请将所有依赖于您的迁移放在 Migration 类的 run_before 属性中:

class Migration(migrations.Migration):
    ...

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

尽可能使用 dependencies 而不是 run_before。 如果在要在编写的迁移之后运行的迁移中指定 dependencies 不合需要或不切实际,则应仅使用 run_before


在第三方应用之间迁移数据

您可以使用数据迁移将数据从一个第三方应用程序移动到另一个应用程序。

如果您打算稍后删除旧应用程序,则需要根据是否安装旧应用程序来设置 dependencies 属性。 否则,卸载旧应用程序后,您将缺少依赖项。 同样,您需要在从旧应用程序检索模型的 apps.get_model() 调用中捕获 LookupError。 这种方法允许您在任何地方部署您的项目,而无需先安装然后卸载旧应用程序。

这是一个示例迁移:

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 将现有表重命名为新表名,同时告诉迁移自动检测器已创建新模型。 可以通过 :djadmin:`sqlmigrate`:djadmin:`dbshell` 查看现有表名。 您可以使用直通模型的 _meta.db_table 属性检查新表名称。 您的新 through 模型应该使用与 Django 相同的 ForeignKey 名称。 此外,如果它需要任何额外的字段,它们应该在 SeparateDatabaseAndState 之后的操作中添加。

例如,如果我们有一个 Book 模型,其中 ManyToManyField 链接到 Author,我们可以添加一个带有新字段 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 可能不适用。