迁移操作

迁移文件由一个或多个 Operation 组成,这些对象声明性地记录了迁移对数据库的作用。

Django 也使用这些 Operation 对象来计算出你的模型在历史上的样子,并计算出自上次迁移以来你对模型做了哪些改变,这样它就可以自动写出你的迁移;这就是为什么它们是声明式的,因为这意味着 Django 可以很容易地将它们全部加载到内存中,并在不接触数据库的情况下运行它们,以计算出你的项目应该是什么样子。

还有更专业的 Operation 对象,用于诸如 数据迁移 和进阶的手动数据库操作。如果你想封装你经常进行的自定义更改,你也可以编写自己的 Operation 类。

如果你需要一个空的迁移文件来编写你自己的 Operation 对象,使用 python manage.py makemigrations --empty yourappname,但是要注意手动添加架构变化的操作会混淆迁移自动检测器,使 makemigrations 的运行结果输出错误的代码。

所有的 Django 核心操作都可以在 django.db.migrations.options 模块中找到。

关于介绍性材料,见 迁移专题指南

架构操作

CreateModel

class CreateModel(name, fields, options=None, bases=None, managers=None)

在项目历史中创建一个新的模型,并在数据库中创建与之匹配的相应表。

name 是模型名称,如在 models.py 文件中写的那样。

fields 是一个由 (field_name, field_instance) 组成的2二元元组的列表。字段实例应该是一个未绑定的字段(所以只是 models.CharField(...),而不是取自另一个模型的字段)。

options 是模型 Meta 类的可选值字典。

bases 是一个可选的其他类的列表,让这个模型继承;它既可以包含类对象,也可以包含格式为 "appname.ModelName" 的字符串,如果你想依赖另一个模型(所以你继承了历史版本)。如果没有提供,它默认为从标准的 models.Model 继承。

managers 需要一个由 (manager_name, manager_instance) 组成的二元元组的列表。在迁移过程中,列表中的第一个管理器将是这个模型的默认管理器。

DeleteModel

class DeleteModel(name)

从项目历史中删除模型,并从数据库中删除它的表。

RenameModel

class RenameModel(old_name, new_name)

将模型从旧名称改名为新名称。

如果你一次更改了模型的名称和相当多的字段,你可能必须手动添加这个;对自动检测器来说,这看起来就像你删除了一个使用旧名称的模型,并添加了一个使用不同名称的新模型,它创建的迁移将丢失旧表中的任何数据。

AlterModelTable

class AlterModelTable(name, table)

更改模型的表名(Meta 子类上的 db_table 选项)。

AlterUniqueTogether

class AlterUniqueTogether(name, unique_together)

改变模型的唯一约束集(Meta 子类上的 unique_together 选项)。

AlterIndexTogether

class AlterIndexTogether(name, index_together)

更改模型的自定义索引集(Meta 子类上的 index_together 选项)。

AlterOrderWithRespectTo

class AlterOrderWithRespectTo(name, order_with_respect_to)

生成或删除 Meta 子类上的 order_with_respect_to 选项所需的 _order 列。

AlterModelOptions

class AlterModelOptions(name, options)

存储对各种模型选项的更改(模型 Meta 上的设置),如 permissionsverbose_name。不影响数据库,但会将这些更改持久化,供 RunPython 实例使用。options 应该是一个将选项名映射到值的字典。

AlterModelManagers

class AlterModelManagers(name, managers)

改变迁移期间可用的管理器。

AddField

class AddField(model_name, name, field, preserve_default=True)

为模型添加一个字段。model_name 是模型的名称,name 是字段的名称,field 是一个未绑定的字段实例(你会在 models.py 中的字段声明中放入的东西——例如,models.IntegerField(null=True))。

preserve_default 参数表示字段的默认值是否是永久的,应该被内置到项目状态中(True),或者它是否是临时的,只适用于这次迁移(False)——通常是因为迁移在表中添加一个不可空的字段,需要一个默认值放到现有的行中。它不影响直接在数据库中设置默认值的行为——Django 从不设置数据库默认值,而总是在 Django ORM 代码中应用。

警告

在旧的数据库中,添加一个具有默认值的字段可能会导致表的完全重写。即使是对于可空字段,也会发生这种情况,可能会对性能产生负面影响。为了避免这种情况,应采取以下步骤。

  • 添加可空字段,不含默认值,并运行 makemigrations 命令。这应该会产生一个带有 AddField 操作的迁移。
  • 将默认值添加到你的字段中,然后运行:djadmin:makemigrations 命令。这应该会生成一个带有 AlterField 操作的迁移。

RemoveField

class RemoveField(model_name, name)

从模型中删除一个字段。

请记住,当反向迁移时,这实际上是向模型添加一个字段。如果字段是可空的,或者它有一个默认值,可以用来填充重新创建的列,那么这个操作是可逆的(除了任何数据损失,这是不可逆的)。如果字段不可为空,也没有默认值,则该操作是不可逆的。

AlterField

class AlterField(model_name, name, field, preserve_default=True)

改变字段的定义,包括改变其类型、nulluniquedb_column 等字段属性。

preserve_default 参数表示字段的默认值是否是永久的,应该被内置到项目状态中(True),或者它是否是临时的,只适用于这次迁移(False)——通常是因为迁移将一个可空的字段改变为不可空的字段,需要一个默认值放到现有的行中。它不影响直接在数据库中设置默认值的行为——Django 从不设置数据库默认值,而总是在 Django ORM 代码中应用。

请注意,并不是所有的数据库都可以进行所有的更改——例如,在大多数数据库中,你不能将像 models.TextField() 这样的文本型字段改为像 models.IntegerField() 这样的数字型字段。

RenameField

class RenameField(model_name, old_name, new_name)

改变一个字段的名称(除非设置了 db_column,否则改变其列名)。

AddIndex

class AddIndex(model_name, index)

在数据库表中为模型创建一个带有 model_name 的索引。index 是 Index 类的一个实例。

RemoveIndex

class RemoveIndex(model_name, name)

从带有 model_name 的模型中删除名为 name 的索引。

RenameIndex

New in Django 4.1.

class RenameIndex(model_name, new_name, old_name=None, old_fields=None)

Renames an index in the database table for the model with model_name. Exactly one of old_name and old_fields can be provided. old_fields is an iterable of the strings, often corresponding to fields of index_together.

On databases that don’t support an index renaming statement (SQLite and MariaDB < 10.5.2), the operation will drop and recreate the index, which can be expensive.

AddConstraint

class AddConstraint(model_name, constraint)

在数据库表中为带有 model_name 的模型创建一个 约束

RemoveConstraint

class RemoveConstraint(model_name, name)

从带有 model_name 的模型中删除名为 name 的约束。

特殊操作

RunSQL

class RunSQL(sql, reverse_sql=None, state_operations=None, hints=None, elidable=False)

允许在数据库上运行任意 SQL——这对于 Django 不直接支持的数据库后端高级功能非常有用。

sqlreverse_sql (如果提供),应该是要在数据库中运行的 SQL 字符串。在大多数数据库后端(除了 PostgreSQL),Django 会在执行 SQL 语句之前将其分割成独立的语句。

警告

在 PostgreSQL 和 SQLite上,只有在 非原子性迁移 中的 SQL 中使用 BEGINCOMMIT,才能避免破坏 Django 的事务状态。

你也可以传递一个字符串或二元元组的列表。后者与 cursor.execute() 一样,用于传递查询和参数。这三种操作是等价的:

  1. migrations.RunSQL("INSERT INTO musician (name) VALUES ('Reinhardt');")
  2. migrations.RunSQL([("INSERT INTO musician (name) VALUES ('Reinhardt');", None)])
  3. migrations.RunSQL([("INSERT INTO musician (name) VALUES (%s);", ['Reinhardt'])])

如果你想在查询中包含字面的百分号,如果你传递的是参数,你必须将它们翻倍。

reverse_sql 查询是在未应用迁移时执行的。它们应该撤销 sql 查询所做的事情。例如,要撤销上面的插入与删除:

  1. migrations.RunSQL(
  2. sql=[("INSERT INTO musician (name) VALUES (%s);", ['Reinhardt'])],
  3. reverse_sql=[("DELETE FROM musician where name=%s;", ['Reinhardt'])],
  4. )

如果 reverse_sqlNone (默认),则 RunSQL 操作是不可逆的。

state_operations 参数允许你提供在项目状态方面相当于 SQL 的操作。例如,如果你正在手动创建一个列,你应该在这里传递一个包含 AddField 操作的列表,这样自动检测器仍然有一个最新的模型状态。如果你不这样做,当你下次运行 makemigrations 时,它不会看到任何添加该字段的操作,所以会尝试再次运行它。例如:

  1. migrations.RunSQL(
  2. "ALTER TABLE musician ADD COLUMN name varchar(255) NOT NULL;",
  3. state_operations=[
  4. migrations.AddField(
  5. 'musician',
  6. 'name',
  7. models.CharField(max_length=255),
  8. ),
  9. ],
  10. )

可选的 hints 参数将作为 **hints 传递给数据库路由器的 allow_migrate() 方法,以帮助它们做出路由决策。参见 提示 了解更多关于数据库提示的细节。

可选的 elidable 参数决定了当 压缩迁移 时,是否会删除(elided)该操作。

RunSQL.noop

当你希望操作在给定的方向上不做任何事情时,将 RunSQL.noop 属性传递给 sqlreverse_sql。这在使操作可逆时特别有用。

RunPython

class RunPython(code, reverse_code=None, atomic=None, hints=None, elidable=False)

在历史上下文中运行自定义 Python 代码。code (如果提供了 reverse_code)应该是可调用的对象,接受两个参数;第一个是 django.app.registry.Apps 的实例,包含与操作在项目历史中的位置相匹配的历史模型,第二个是 SchemaEditor 的实例。

reverse_code 参数在取消应用迁移时被调用。这个可调用对象参数应该撤销在 code 可调用对象参数中所做的事情,这样迁移才是可逆的。如果 reverse_code 是``None`` (默认),则 RunPython 操作是不可逆的。

可选的 hints 参数将作为 **hints 传递给数据库路由器的 allow_migrate() 方法,以帮助它们做出路由决策。参见 提示 了解更多关于数据库提示的细节。

可选的 elidable 参数决定了当 压缩迁移 时,是否会删除(elided)该操作。

建议你把这些代码写成一个单独的函数,放在迁移文件中的 Migration 类上面,然后传递给 RunPython。下面是一个使用 RunPythonCountry 模型上创建一些初始对象的例子:

  1. from django.db import migrations
  2. def forwards_func(apps, schema_editor):
  3. # We get the model from the versioned app registry;
  4. # if we directly import it, it'll be the wrong version
  5. Country = apps.get_model("myapp", "Country")
  6. db_alias = schema_editor.connection.alias
  7. Country.objects.using(db_alias).bulk_create([
  8. Country(name="USA", code="us"),
  9. Country(name="France", code="fr"),
  10. ])
  11. def reverse_func(apps, schema_editor):
  12. # forwards_func() creates two Country instances,
  13. # so reverse_func() should delete them.
  14. Country = apps.get_model("myapp", "Country")
  15. db_alias = schema_editor.connection.alias
  16. Country.objects.using(db_alias).filter(name="USA", code="us").delete()
  17. Country.objects.using(db_alias).filter(name="France", code="fr").delete()
  18. class Migration(migrations.Migration):
  19. dependencies = []
  20. operations = [
  21. migrations.RunPython(forwards_func, reverse_func),
  22. ]

这通常是你用来创建 数据迁移 的操作,运行自定义数据更新和更改,以及其他任何你需要访问 ORM 和/或 Python 代码的操作。

就像 RunSQL 一样,确保如果你在这里改变模式,你要么是在 Django 模型系统的范围外进行(例如触发器),要么你使用 SeparateDatabaseAndState 来添加操作,以反映你对模型状态的改变——否则,过时的 ORM 和自动检测器将停止正常工作。

默认情况下,RunPython 将在不支持 DDL 事务的数据库(例如 MySQL 和 Oracle)的事务中运行其内容。这应该是安全的,但如果你试图使用这些后端提供的 schema_editor 可能会导致崩溃;在这种情况下,将 atomic=False 传递给 RunPython 操作。

在支持 DDL 事务的数据库上(SQLite 和 PostgreSQL),RunPython 操作除了为每次迁移创建的事务外,不会自动添加任何事务。因此,例如在 PostgreSQL 上,你应该避免在同一个迁移中把架构变化和 RunPython 操作结合起来,否则你可能会遇到 OperationalError: cannot ALTER TABLE "mytable" because it has pending trigger events 这样的错误。

如果你有一个不同的数据库,并且不确定它是否支持 DDL 事务,检查 django.db.connection.features.can_rollback_ddl 属性。

如果 RunPython 操作是 非原子性的迁移 的一部分,那么只有当 atomic=True 传递给 RunPython 操作时,该操作才会在事务中执行。

警告

RunPython 不会为你神奇地改变模型的连接;你调用的任何模型方法将转到默认的数据库,除非你给它们当前的数据库别名(可从 schema_editor.connection.alias 中获得,其中 schema_editor 是你函数的第二个参数)。

static RunPython.noop()

当你希望操作在给定的方向上不做任何事情时,将 RunPython.noop 方法传递给 codereverse_code。这在使操作可逆时特别有用。

SeparateDatabaseAndState

class SeparateDatabaseAndState(database_operations=None, state_operations=None)

一个高度专业化的操作,让你混合和匹配数据库(架构改变)和状态(自动检测器支持)方面的操作。

它接受两个操作列表。当要求它应用状态时,它将使用 state_operations 列表(这是 RunSQLstate_operations 参数的通用版本)。当要求它对数据库进行更改时,它将使用 database_operations 列表。

如果数据库的实际状态和 Django 的状态视图不同步,就会破坏迁移框架,甚至导致数据丢失。值得谨慎行事,仔细检查你的数据库和状态操作。你可以使用 sqlmigratedbshell 来检查你的数据库操作。你可以使用 makemigrations,特别是使用 --dry-run,来检查你的状态操作。

关于使用 SeparateDatabaseAndState 的例子,请参见 changing-a-manytomanyfield to use-a-through-model。

自己写

操作有一个相对简单的 API,而且它们被设计成可以让你很容易地编写自己的 API 来补充内置的 Django 的 API。一个 Operation 的基本结构是这样的:

  1. from django.db.migrations.operations.base import Operation
  2. class MyCustomOperation(Operation):
  3. # If this is False, it means that this operation will be ignored by
  4. # sqlmigrate; if true, it will be run and the SQL collected for its output.
  5. reduces_to_sql = False
  6. # If this is False, Django will refuse to reverse past this operation.
  7. reversible = False
  8. def __init__(self, arg1, arg2):
  9. # Operations are usually instantiated with arguments in migration
  10. # files. Store the values of them on self for later use.
  11. pass
  12. def state_forwards(self, app_label, state):
  13. # The Operation should take the 'state' parameter (an instance of
  14. # django.db.migrations.state.ProjectState) and mutate it to match
  15. # any schema changes that have occurred.
  16. pass
  17. def database_forwards(self, app_label, schema_editor, from_state, to_state):
  18. # The Operation should use schema_editor to apply any changes it
  19. # wants to make to the database.
  20. pass
  21. def database_backwards(self, app_label, schema_editor, from_state, to_state):
  22. # If reversible is True, this is called when the operation is reversed.
  23. pass
  24. def describe(self):
  25. # This is used to describe what the operation does in console output.
  26. return "Custom Operation"
  27. @property
  28. def migration_name_fragment(self):
  29. # Optional. A filename part suitable for automatically naming a
  30. # migration containing this operation, or None if not applicable.
  31. return "custom_operation_%s_%s" % (self.arg1, self.arg2)

你可以使用这个模板,并在此基础上进行工作,不过我们建议查看 django.db.migrations.options 中内置的 Django 操作——它们涵盖了很多迁移框架半内部方面的例子,比如 ProjectState 和用于获取历史模型的模式,以及 ModelStatestate_forwards() 中用于突变历史模型的模式。

注意事项:

  • 你不需要学习太多关于 ProjectState 的知识来编写迁移;只需要知道它有一个 apps 属性,它提供了对应用程序注册表的访问(然后你可以调用 get_model)。

  • database_forwardsdatabase_backwards 都有两个状态传递给它们;这些状态代表了 state_forwards 方法本会应用的差异,但为了方便和速度的原因,给了你。

  • 如果你想从 database_forwards()database_backwards() 中的 from_state 参数中处理模型类或模型实例,你必须使用 clear_delayed_apps_cache() 方法渲染模型状态,以使相关模型可用:

    1. def database_forwards(self, app_label, schema_editor, from_state, to_state):
    2. # This operation should have access to all models. Ensure that all models are
    3. # reloaded in case any are delayed.
    4. from_state.clear_delayed_apps_cache()
    5. ...
  • database_backwards 方法中的 to_state 是指 的状态;也就是反向迁移完成后将成为当前状态的状态。

  • 你可能会在内置操作上看到 references_model 的实现,这是自动检测代码的一部分,对自定义操作并不重要。

警告

由于性能原因,ModelState.fields.Field 中的 Field` 实例会在不同的迁移中重复使用。你决不能改变这些实例的属性。如果你需要在 state_forwards() 中突变一个字段,你必须从 ModelState.fields 中删除旧的实例,并在其位置上添加一个新的实例。对于 ModelState.managers 中的 Manager` 实例也是如此。

举个例子,让我们做一个加载 PostgreSQL 扩展的操作(其中包含了 PostgreSQL 的一些更令人兴奋的特性)。由于没有改变模型状态,所以它所做的只是运行一条命令:

  1. from django.db.migrations.operations.base import Operation
  2. class LoadExtension(Operation):
  3. reversible = True
  4. def __init__(self, name):
  5. self.name = name
  6. def state_forwards(self, app_label, state):
  7. pass
  8. def database_forwards(self, app_label, schema_editor, from_state, to_state):
  9. schema_editor.execute("CREATE EXTENSION IF NOT EXISTS %s" % self.name)
  10. def database_backwards(self, app_label, schema_editor, from_state, to_state):
  11. schema_editor.execute("DROP EXTENSION %s" % self.name)
  12. def describe(self):
  13. return "Creates extension %s" % self.name
  14. @property
  15. def migration_name_fragment(self):
  16. return "create_extension_%s" % self.name