如何创建数据库迁移
本文介绍了如何为可能遇到的不同场景组织和编写数据库迁移。关于迁移的介绍性资料,参考 专题指南。
数据迁移和多种数据库
使用多种数据库时,你可能需要指定是否为特定数据库运行迁移。例如,你可能 只想 为特定数据库运行迁移。
为此,你可以在 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"}),
]
若你的 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,前者在旧应用程序中检索模型。这种方法允许你在任何地方部署项目,而无需先安装并且卸载旧应用程序。
这是一个迁移示例:
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
字段。
如果您将类 ManyToManyField 更改为使用 through
模型,默认迁移将删除现有表并创建新表,从而丢失现有关系。 为避免这种情况,您可以使用类 SeparateDatabaseAndState 将现有表重命名为新表名,同时告诉迁移自动检测器已创建新模型。 您可以通过 sqlmigrate 或 dbshell 检查现有表名。您可以使用直通模型的 _meta.db_table
属性检查新表名称。 您的新 through
模型应该使用与 Django 相同的 ForeignKey
s 名称。 此外,如果它需要任何额外的字段,它们应该在类 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
的修改操作不会被执行。