如何创建数据库迁移

本文档说明了如何为可能遇到的不同场景构建和编写数据库迁移。有关迁移的入门资料,请参阅主题指南

数据迁移和多个数据库

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

为此,您可以通过查看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()方法。

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

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

添加唯一字段的迁移

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

因此,应采取以下步骤。在此示例中,我们将添加一个具有默认值的非空UUIDField。根据您的需要修改相应的字段。

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

  • 运行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),
        ]
    
  • 现在,您可以像往常一样使用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 的原子 DDL 语句支持指的是单个语句,而不是可以回滚的多个语句包装在事务中。)

控制迁移的顺序

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

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

dependencies属性的声明方式如下

from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
    ]

通常这已经足够了,但有时您可能需要确保您的迁移在其他迁移之前运行。例如,这对于使第三方应用程序的迁移在您的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 将现有表重命名为新的表名,同时告诉迁移自动检测器已创建了新模型。您可以通过 sqlmigratedbshell 检查现有表名。您可以使用 through 模型的 _meta.db_table 属性检查新表名。您的新 through 模型应使用与 Django 相同的 ForeignKey 名称。此外,如果它需要任何额外的字段,则应在 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 操作的迁移中的模式更改可能不会应用。

返回顶部