编写数据库迁移语句¶
本文介绍了如何为可能遇到的不同场景组织和编写数据库迁移。关于迁移的介绍性资料,参考 专题指南。
数据迁移和多种数据库¶
使用多种数据库时,你可能需要指定是否为特定数据库运行迁移。例如,你可能 只想 为特定数据库运行迁移。
为此,你可以在 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() 方法的提示:
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'}),
    ]
若你的 RunPython 或 RunSQL 操作只影响了一个模型,为其传入 model_name 作为提示,使其对路由器更加透明。这对可复用的和第三方应用特别重要。
添加独一无二字段的迁移¶
应用 “普通” 迁移,将新的唯一非空的字段添加到已拥有一些行的表格会抛出一个错误,因为用于填充现有行的值只生成一次,从而打破了唯一约束。唯一非空字段即所有行的该字段都不能为空,且值唯一,不能重复。
因此,需要做以下步骤。在本例中,我们将添加一个带默认值的非空 UUIDField。根据你的需要修改对应字段。
- 在模型中以 - default=uuid.uuid4和- unique=True参数添加该字段(根据字段类型,为其选择一个合适的默认值)。
- 运行 - makemigrations命令。这将生成一个- AddField操作的迁移。
- 通过运行 - makemigrations myapp --empty两次为同一应用生成两个相同的空迁移文件。我们已在以下例子中将迁移文件重命名成有意义的名字。
- 从自动生成的迁移(3个新文件中的第一个)中将 - AddField操作拷贝至上一个迁移,将- AddField改为- AlterField,添加- uuid和- models的导入。例子: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 字段,并延迟创建唯一性约束,直到我们已为所以行填充了唯一值。
- 在第一个空的迁移文件中,添加一个 - RunPython或- RunSQL操作,为每个已存在的行创建一个唯一值(本例中 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 不是通过迁移的名字决定迁移执行顺序,而是通过在 迁移 类上使用两个属性:  dependencies 和 run_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,前者在旧应用程序中检索模型。这种方法允许你在任何地方部署项目,而无需先安装并且卸载旧应用程序。
这是一个迁移示例:
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 字段。¶
假如你通过使用 through 模型来改变  ManyToManyField ,默认的迁移操作会删除现存的表并创建一个新的表,来解耦现存的关联。为了避免这种情况,你可以使用 SeparateDatabaseAndState  来重命名现存的表为另一个新的表名,同时告诉迁移自动探测器新的模型已经被建立了。你可以通过 sqlmigrate 或 dbshell 来查看现有的表名。你可以通过模型的 _meta.db_table 属性来检查新的表名。新的 through  模型应该使用与那些 外键 们相同的名称。同时,假如需要其他额外的字段,它们应该添加在  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 的修改操作不会被执行。
 
          