编写数据库迁移语句

本文介绍了如何为可能遇到的不同场景组织和编写数据库迁移。关于迁移的介绍性资料,参考 专题指南

数据迁移和多种数据库

使用多种数据库时,你可能需要指定是否为特定数据库运行迁移。例如,你可能 只想 为特定数据库运行迁移。

为此,你可以在 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),
    ]

你也能提供会以 **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 两次为同一应用生成两个相同的空迁移文件。我们已在以下例子中将迁移文件重命名成有意义的名字。

  • 从自动生成的迁移(3个新文件中的第一个)中将 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——这将创建中间 null 字段,并延迟创建唯一性约束,直到我们已为所以行填充了唯一值。

  • 在第一个空的迁移文件中,添加一个 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 and PostgreSQL),迁移默认运行在事务内。对于类似在大数据表上运行数据迁移的场景,你可以通过将 atomic 属性置为 False 避免在事务中运行迁移:

from django.db import migrations

class Migration(migrations.Migration):
    atomic = False

在这样的迁移种,所有的操作运行时都不含事务。通过使用 atomic() 或为 RunPython 传入 atomic=True 能将部分迁移置于事务之中。

这是一个例子,关于非原子性数据迁移操作,将更新大数据表的操作分为数个小批次:

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 不是通过迁移的名字决定迁移执行顺序,而是通过在 迁移 类上使用两个属性: dependenciesrun_before

若你用过 makemigrations 命令,你可能早已在运行时见过 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

在第三方应用程序中迁移数据

你可以使用数据迁移把数据从一个第三方应用程序中转移到另一个。

如果你计划要移除旧应用程序,则需要根据是否安装旧应用程序来设置 依赖 属性。否则,一旦你卸载旧应用程序,就会缺失依赖项。同样,你需要在调用 app.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 操作的第二个参数。

通过使用 through 模型来更改 ManyToManyField 字段。

If you change a ManyToManyField to use a through model, the default migration will delete the existing table and create a new one, losing the existing relations. To avoid this, you can use SeparateDatabaseAndState to rename the existing table to the new table name while telling the migration autodetector that the new model has been created. You can check the existing table name through sqlmigrate or dbshell. You can check the new table name with the through model's _meta.db_table property. Your new through model should use the same names for the ForeignKeys as Django did. Also if it needs any extra fields, they should be added in operations after SeparateDatabaseAndState.

例如,假如你有一个 Book 模型,它通过 ManyToManyField  链接 Author 模型,我们可以通过像下面这样添加一个带有新字段 is_primary 的中间模型 AuthorBook

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 的修改操作不会被执行。