Skip to main content

编写数据库迁移

本文档介绍了如何为可能遇到的不同情况构建和写入数据库迁移。有关迁移的介绍材料,请参阅 主题指南

数据迁移和多个数据库

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

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

from django.db import migrations

def forwards(apps, schema_editor):
    if not 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(object):

    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。例如:

    0006_remove_uuid_null.py
    # -*- coding: utf-8 -*-
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from __future__ import unicode_literals
    
    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)。例如:

    0005_populate_uuid_values.py
    # -*- coding: utf-8 -*-
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from __future__ import unicode_literals
    
    from django.db import migrations, models
    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()
    
    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 覆盖。

非原子迁移

New in Django 1.10.

在支持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)没有影响。

控制迁移的顺序

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

如果你使用了 makemigrations 命令,你可能已经看到 dependencies 在操作,因为自动创建的迁移已经定义为它们的创建过程的一部分。

dependencies 属性声明如下:

from django.db import migrations

class Migration(migrations.Migration):

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

通常这是足够的,但是有时可能需要确保您的迁移运行 before 其他迁移。这是有用的,例如,使第三方应用程序的迁移运行 after 您的 AUTH_USER_MODEL 替换。

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

class Migration(migrations.Migration):
    ...

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

如果可能,优先使用 dependenciesrun_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 操作的第二个参数。

将非托管模型更改为托管模型

如果要将非托管模型(managed=False)更改为托管模式,则必须在对模型进行其他模式相关更改之前删除 managed=False 并生成迁移,因为包含用于更改 Meta.managed 的操作的迁移中出现的模式更改可能不会应用。