如何创建数据库迁移

本文档解释了如何为可能遇到的不同场景构建和编写数据库迁移。有关迁移的介绍性材料,请参见 the topic guide .

数据迁移和多个数据库

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

为此,可以检查数据库连接的别名 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"}),
    ]

如果你 RunPythonRunSQL 操作只影响一个模型,通过是很好的做法 model_name 作为一个提示,使它对路由器尽可能透明。这对于可重用和第三方应用程序尤其重要。

添加唯一字段的迁移

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

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

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

  • 运行 makemigrations 命令。这将生成一个带有 AddField 操作。

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

  • 复制 AddField 从自动生成的迁移(三个新文件中的第一个)到最后一个迁移的操作,更改 AddFieldAlterField ,并添加 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=Truenull=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=TrueRunPython .

下面是一个非原子数据迁移的示例,它以较小的批处理更新一个大表:

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 类: dependenciesrun_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 基于是否安装旧应用的属性。否则,一旦卸载旧应用程序,就会丢失依赖项。同样,你也需要抓住 LookupErrorapps.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 使用A through 模型

如果你改变 ManyToManyField 使用A through 模型时,默认迁移将删除现有表并创建新表,丢失现有关系。为了避免这种情况,可以使用 SeparateDatabaseAndState 将现有表重命名为新表名,同时通知迁移自动检测器新模型已创建。您可以通过检查现有表名 sqlmigratedbshell . 您可以使用through模型的 _meta.db_table 财产。你的新产品 through 模型应使用相同的名称 ForeignKey 就像Django做的那样。另外,如果它需要任何额外的字段,它们应该添加到操作之后 SeparateDatabaseAndState .

例如,如果我们有一个 Book 模型与A 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 可能不适用。