迁移操作

迁移文件由一个或多个 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 选项)。

AlterModelTableComment

New in Django 4.2.
class AlterModelTableComment(name, table_comment)

Changes the model's table comment (the db_table_comment option on the Meta subclass).

AlterUniqueTogether

class AlterUniqueTogether(name, unique_together)

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

AlterIndexTogether

class AlterIndexTogether(name, index_together)

Changes the model's set of custom indexes (the index_together option on the Meta subclass).

警告

AlterIndexTogether is officially supported only for pre-Django 4.2 migration files. For backward compatibility reasons, it's still part of the public API, and there's no plan to deprecate or remove it, but it should not be used for new migrations. Use AddIndex and RemoveIndex operations instead.

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() 一样,用于传递查询和参数。这三种操作是等价的:

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

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

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

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

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

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

migrations.RunSQL(
    "ALTER TABLE musician ADD COLUMN name varchar(255) NOT NULL;",
    state_operations=[
        migrations.AddField(
            "musician",
            "name",
            models.CharField(max_length=255),
        ),
    ],
)

可选的 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 模型上创建一些初始对象的例子:

from django.db import migrations


def forwards_func(apps, schema_editor):
    # We get the model from the versioned app registry;
    # if we directly import it, it'll be the wrong version
    Country = apps.get_model("myapp", "Country")
    db_alias = schema_editor.connection.alias
    Country.objects.using(db_alias).bulk_create(
        [
            Country(name="USA", code="us"),
            Country(name="France", code="fr"),
        ]
    )


def reverse_func(apps, schema_editor):
    # forwards_func() creates two Country instances,
    # so reverse_func() should delete them.
    Country = apps.get_model("myapp", "Country")
    db_alias = schema_editor.connection.alias
    Country.objects.using(db_alias).filter(name="USA", code="us").delete()
    Country.objects.using(db_alias).filter(name="France", code="fr").delete()


class Migration(migrations.Migration):
    dependencies = []

    operations = [
        migrations.RunPython(forwards_func, reverse_func),
    ]

这通常是你用来创建 数据迁移 的操作,运行自定义数据更新和更改,以及其他任何你需要访问 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 的基本结构是这样的:

from django.db.migrations.operations.base import Operation


class MyCustomOperation(Operation):
    # If this is False, it means that this operation will be ignored by
    # sqlmigrate; if true, it will be run and the SQL collected for its output.
    reduces_to_sql = False

    # If this is False, Django will refuse to reverse past this operation.
    reversible = False

    def __init__(self, arg1, arg2):
        # Operations are usually instantiated with arguments in migration
        # files. Store the values of them on self for later use.
        pass

    def state_forwards(self, app_label, state):
        # The Operation should take the 'state' parameter (an instance of
        # django.db.migrations.state.ProjectState) and mutate it to match
        # any schema changes that have occurred.
        pass

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        # The Operation should use schema_editor to apply any changes it
        # wants to make to the database.
        pass

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        # If reversible is True, this is called when the operation is reversed.
        pass

    def describe(self):
        # This is used to describe what the operation does in console output.
        return "Custom Operation"

    @property
    def migration_name_fragment(self):
        # Optional. A filename part suitable for automatically naming a
        # migration containing this operation, or None if not applicable.
        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() 方法渲染模型状态,以使相关模型可用:

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

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

警告

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

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

from django.db.migrations.operations.base import Operation


class LoadExtension(Operation):
    reversible = True

    def __init__(self, name):
        self.name = name

    def state_forwards(self, app_label, state):
        pass

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        schema_editor.execute("CREATE EXTENSION IF NOT EXISTS %s" % self.name)

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        schema_editor.execute("DROP EXTENSION %s" % self.name)

    def describe(self):
        return "Creates extension %s" % self.name

    @property
    def migration_name_fragment(self):
        return "create_extension_%s" % self.name