如何创建数据库迁移

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

数据迁移和多种数据库

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

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

  1. from django.db import migrations
  2. def forwards(apps, schema_editor):
  3. if schema_editor.connection.alias != "default":
  4. return
  5. # Your migration code goes here
  6. class Migration(migrations.Migration):
  7. dependencies = [
  8. # Dependencies to other migrations
  9. ]
  10. operations = [
  11. migrations.RunPython(forwards),
  12. ]

你也能提供会以 **hints 传递给数据库路由器的 allow_migrate() 方法的提示:

myapp/dbrouters.py

  1. class MyRouter:
  2. def allow_migrate(self, db, app_label, model_name=None, **hints):
  3. if "target_db" in hints:
  4. return db == hints["target_db"]
  5. return True

然后,要将其在迁移中生效,像下面这样做:

  1. from django.db import migrations
  2. def forwards(apps, schema_editor):
  3. # Your migration code goes here
  4. ...
  5. class Migration(migrations.Migration):
  6. dependencies = [
  7. # Dependencies to other migrations
  8. ]
  9. operations = [
  10. migrations.RunPython(forwards, hints={"target_db": "default"}),
  11. ]

若你的 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

  1. class Migration(migrations.Migration):
  2. dependencies = [
  3. ("myapp", "0005_populate_uuid_values"),
  4. ]
  5. operations = [
  6. migrations.AlterField(
  7. model_name="mymodel",
  8. name="uuid",
  9. field=models.UUIDField(default=uuid.uuid4, unique=True),
  10. ),
  11. ]
  12. ```
  • 编辑第一个迁移文件。生成的迁移类应该看起来像这样:

    0004_add_uuid_field.py

    1. class Migration(migrations.Migration):
    2. dependencies = [
    3. ("myapp", "0003_auto_20150129_1705"),
    4. ]
    5. operations = [
    6. migrations.AddField(
    7. model_name="mymodel",
    8. name="uuid",
    9. field=models.UUIDField(default=uuid.uuid4, unique=True),
    10. ),
    11. ]

    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

  1. def gen_uuid(apps, schema_editor):
  2. MyModel = apps.get_model("myapp", "MyModel")
  3. for row in MyModel.objects.all():
  4. row.uuid = uuid.uuid4()
  5. row.save(update_fields=["uuid"])
  6. class Migration(migrations.Migration):
  7. dependencies = [
  8. ("myapp", "0004_add_uuid_field"),
  9. ]
  10. operations = [
  11. # omit reverse_code=... if you don't want the migration to be reversible.
  12. migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
  13. ]
  14. ```
  • 现在你能像往常一样用 migrate 应用迁移了。

    注意,若你允许运行迁移时创建对象可能会造成竞争。 AddField 后和 RunPython 前创建的对象保留原先重写的 uuid 值。

非原子性迁移

对于支持 DDL 事务的数据库 (SQLite and PostgreSQL),迁移默认运行在事务内。对于类似在大数据表上运行数据迁移的场景,你可以通过将 atomic 属性置为 False 避免在事务中运行迁移:

  1. from django.db import migrations
  2. class Migration(migrations.Migration):
  3. atomic = False

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

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

  1. import uuid
  2. from django.db import migrations, transaction
  3. def gen_uuid(apps, schema_editor):
  4. MyModel = apps.get_model("myapp", "MyModel")
  5. while MyModel.objects.filter(uuid__isnull=True).exists():
  6. with transaction.atomic():
  7. for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
  8. row.uuid = uuid.uuid4()
  9. row.save()
  10. class Migration(migrations.Migration):
  11. atomic = False
  12. operations = [
  13. migrations.RunPython(gen_uuid),
  14. ]

atomic 属性对不支持 DDL 事务的数据库没有影响(例如 MySQL,Oracle)。(MySQL 的 原子性 DDL 语句支持 指向独立的语句,而不是封装在能回滚的事务中的多句语句。)

控制迁移顺序

Django 不是通过迁移的名字决定迁移执行顺序,而是通过在 迁移 类上使用两个属性: dependenciesrun_before

若你用过 makemigrations 命令,你可能早已在运行时见过 dependencies,因为自动创建的迁移将此定义为其创建过程的一部分。

依赖 属性像这样申明:

  1. from django.db import migrations
  2. class Migration(migrations.Migration):
  3. dependencies = [
  4. ("myapp", "0123_the_previous_migration"),
  5. ]

通常这就够用了,但是有很多次,你总是需要确认你的迁移运行在其它迁移 之前。例如,这对于让第三方应用的迁移运行在替换 AUTH_USER_MODEL 之后就很有用。

要实现此目的,将所有需要先运行的迁移置于你的 Migration 类的 run_before 属性:

  1. class Migration(migrations.Migration):
  2. ...
  3. run_before = [
  4. ("third_party_app", "0001_do_awesome"),
  5. ]

尽可能使用 dependencies,而不是 run_before。只有在在特定迁移中添加 dependencies 使其运行于你编写的迁移之后是没希望的和不切实际的情况下,你才能使用 run_before

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

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

如果你计划要移除旧应用程序,则需要根据是否安装旧应用程序来设置 依赖 属性。否则,一旦你卸载旧应用程序,就会缺失依赖项。同样,你需要在调用 app.get_model() 时捕获 LookupError,前者在旧应用程序中检索模型。这种方法允许你在任何地方部署项目,而无需先安装并且卸载旧应用程序。

这是一个迁移示例:

myapp/migrations/0124_move_old_app_to_new_app.py

  1. from django.apps import apps as global_apps
  2. from django.db import migrations
  3. def forwards(apps, schema_editor):
  4. try:
  5. OldModel = apps.get_model("old_app", "OldModel")
  6. except LookupError:
  7. # The old app isn't installed.
  8. return
  9. NewModel = apps.get_model("new_app", "NewModel")
  10. NewModel.objects.bulk_create(
  11. NewModel(new_attribute=old_object.old_attribute)
  12. for old_object in OldModel.objects.all()
  13. )
  14. class Migration(migrations.Migration):
  15. operations = [
  16. migrations.RunPython(forwards, migrations.RunPython.noop),
  17. ]
  18. dependencies = [
  19. ("myapp", "0123_the_previous_migration"),
  20. ("new_app", "0001_initial"),
  21. ]
  22. if global_apps.is_installed("old_app"):
  23. dependencies.append(("old_app", "0001_initial"))

另外在迁移未执行时,请考虑好什么是你想要发生的。你可以什么都不做(就像上面的示例)或者从新应用中移除一些或全部的数据。相应的调整 RunPython 操作的第二个参数。

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

如果您将类 ManyToManyField 更改为使用 through 模型,默认迁移将删除现有表并创建新表,从而丢失现有关系。 为避免这种情况,您可以使用类 SeparateDatabaseAndState 将现有表重命名为新表名,同时告诉迁移自动检测器已创建新模型。 您可以通过 sqlmigratedbshell 检查现有表名。您可以使用直通模型的 _meta.db_table 属性检查新表名称。 您的新 through 模型应该使用与 Django 相同的 ForeignKeys 名称。 此外,如果它需要任何额外的字段,它们应该在类 SeparateDatabaseAndState 之后添加到操作中。

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

  1. from django.db import migrations, models
  2. import django.db.models.deletion
  3. class Migration(migrations.Migration):
  4. dependencies = [
  5. ("core", "0001_initial"),
  6. ]
  7. operations = [
  8. migrations.SeparateDatabaseAndState(
  9. database_operations=[
  10. # Old table name from checking with sqlmigrate, new table
  11. # name from AuthorBook._meta.db_table.
  12. migrations.RunSQL(
  13. sql="ALTER TABLE core_book_authors RENAME TO core_authorbook",
  14. reverse_sql="ALTER TABLE core_authorbook RENAME TO core_book_authors",
  15. ),
  16. ],
  17. state_operations=[
  18. migrations.CreateModel(
  19. name="AuthorBook",
  20. fields=[
  21. (
  22. "id",
  23. models.AutoField(
  24. auto_created=True,
  25. primary_key=True,
  26. serialize=False,
  27. verbose_name="ID",
  28. ),
  29. ),
  30. (
  31. "author",
  32. models.ForeignKey(
  33. on_delete=django.db.models.deletion.DO_NOTHING,
  34. to="core.Author",
  35. ),
  36. ),
  37. (
  38. "book",
  39. models.ForeignKey(
  40. on_delete=django.db.models.deletion.DO_NOTHING,
  41. to="core.Book",
  42. ),
  43. ),
  44. ],
  45. ),
  46. migrations.AlterField(
  47. model_name="book",
  48. name="authors",
  49. field=models.ManyToManyField(
  50. to="core.Author",
  51. through="core.AuthorBook",
  52. ),
  53. ),
  54. ],
  55. ),
  56. migrations.AddField(
  57. model_name="authorbook",
  58. name="is_primary",
  59. field=models.BooleanField(default=False),
  60. ),
  61. ]

将非托管模型变为托管的

如果你想要将非托管模型 (managed=False) 变为托管的,你必须移除 managed=False 并且在对此模型做其他模式相关的改变前生成一次迁移,因为如果迁移中出现模式改变,对 Meta.managed 的修改操作不会被执行。