迁移 — Django 文档
迁移
迁移是 Django 将您对模型所做的更改(添加字段、删除模型等)传播到数据库架构中的方式。 它们被设计为大部分是自动的,但您需要知道何时进行迁移、何时运行它们以及您可能遇到的常见问题。
命令
您将使用几个命令与迁移和 Django 对数据库架构的处理进行交互:
- :djadmin:`migrate`,负责应用和取消应用迁移。
- :djadmin:`makemigrations`,它负责根据您对模型所做的更改创建新的迁移。
- :djadmin:`sqlmigrate`,显示迁移的 SQL 语句。
- :djadmin:`showmigrations`,列出项目的迁移及其状态。
您应该将迁移视为数据库架构的版本控制系统。 makemigrations
负责将您的模型更改打包到单独的迁移文件中 - 类似于提交 - 而 migrate
负责将这些更改应用于您的数据库。
每个应用程序的迁移文件都位于该应用程序内的“迁移”目录中,旨在提交到其代码库并作为其代码库的一部分进行分发。 您应该在开发机器上进行一次迁移,然后在同事的机器、临时机器上运行相同的迁移,最终在生产机器上运行。
迁移将在相同的数据集上以相同的方式运行并产生一致的结果,这意味着您在开发和暂存中看到的内容与在相同情况下将在生产中发生的完全相同。
Django 将对您的模型或字段的任何更改进行迁移 - 即使是不影响数据库的选项 - 因为它可以正确重建字段的唯一方法是在历史记录中进行所有更改,您可能需要这些选项稍后进行一些数据迁移(例如,如果您设置了自定义验证器)。
后端支持
Django 附带的所有后端以及任何第三方后端都支持迁移,前提是它们已进行编程以支持模式更改(通过 SchemaEditor 类完成)。
但是,在架构迁移方面,某些数据库比其他数据库更强大; 下面介绍了一些注意事项。
PostgreSQL
就模式支持而言,PostgreSQL 是这里所有数据库中功能最强大的。
唯一需要注意的是,在 PostgreSQL 11 之前,添加具有默认值的列会导致表的完全重写,时间与其大小成正比。 因此,建议您始终使用 null=True
创建新列,因为这样它们会立即添加。
MySQL
MySQL 不支持围绕模式更改操作的事务,这意味着如果迁移失败,您将不得不手动取消更改以便重试(不可能回滚到更早的点)。
此外,MySQL 将针对几乎每个模式操作完全重写表,并且通常需要与表中的行数成比例的时间来添加或删除列。 在较慢的硬件上,这可能比每百万行一分钟更糟糕 - 在只有几百万行的表中添加几列可能会将您的站点锁定超过十分钟。
最后,MySQL 对列、表和索引的名称长度的限制相对较小,并且对索引涵盖的所有列的组合大小也有限制。 这意味着在其他后端可能的索引将无法在 MySQL 下创建。
SQLite
SQLite 几乎没有内置的模式更改支持,因此 Django 尝试通过以下方式模拟它:
- 使用新架构创建新表
- 复制数据
- 丢弃旧桌子
- 重命名新表以匹配原始名称
这个过程通常运行良好,但它可能很慢并且偶尔会出现错误。 除非您非常了解风险及其局限性,否则不建议您在生产环境中运行和迁移 SQLite; Django 附带的支持旨在允许开发人员在他们的本地机器上使用 SQLite 来开发不太复杂的 Django 项目,而无需完整的数据库。
工作流程
使用迁移很简单。 对您的模型进行更改 - 例如,添加一个字段并删除一个模型 - 然后运行 :djadmin:`makemigrations`:
$ python manage.py makemigrations
Migrations for 'books':
books/migrations/0003_auto.py:
- Alter field author on book
您的模型将被扫描并与迁移文件中当前包含的版本进行比较,然后将写出一组新的迁移。 请务必阅读输出以查看 makemigrations
认为您已更改的内容 - 这并不完美,对于复杂的更改,它可能无法检测到您期望的内容。
一旦你有了新的迁移文件,你应该将它们应用到你的数据库以确保它们按预期工作:
$ python manage.py migrate
Operations to perform:
Apply all migrations: books
Running migrations:
Rendering model states... DONE
Applying books.0003_auto... OK
应用迁移后,将迁移和模型更改作为单个提交提交到您的版本控制系统 - 这样,当其他开发人员(或您的生产服务器)检查代码时,他们将同时获得对模型的更改以及伴随的迁移。
如果您想为迁移指定一个有意义的名称而不是生成的名称,您可以使用 makemigrations --name
选项:
$ python manage.py makemigrations --name changed_my_model your_app_label
版本控制
由于迁移存储在版本控制中,您偶尔会遇到这样的情况:您和另一个开发人员同时提交了对同一个应用程序的迁移,从而导致两次迁移的编号相同。
别担心 - 这些数字仅供开发人员参考,Django 只关心每个迁移都有不同的名称。 迁移在文件中指定了它们依赖的其他迁移 - 包括同一应用程序中较早的迁移 - 因此可以检测何时存在同一应用程序的两个未订购的新迁移。
发生这种情况时,Django 会提示您并提供一些选项。 如果它认为足够安全,它会为您自动线性化两次迁移。 如果没有,您将不得不自己修改迁移——别担心,这并不困难,下面的 迁移文件 中有更多解释。
依赖关系
虽然迁移是针对每个应用程序的,但您的模型所隐含的表和关系过于复杂,无法一次只为一个应用程序创建。 当您进行需要运行其他操作的迁移时 - 例如,您将 books
应用程序中的 ForeignKey
添加到 authors
应用程序 - 生成的迁移将包含依赖项关于 authors
中的迁移。
这意味着当您运行迁移时,authors
迁移首先运行并创建 ForeignKey
引用的表,然后使 ForeignKey
列运行的迁移随后运行并创建约束。 如果没有发生这种情况,迁移将尝试创建 ForeignKey
列,而它引用的表不存在,并且您的数据库将引发错误。
此依赖行为会影响您限制为单个应用程序的大多数迁移操作。 仅限于单个应用程序(在 makemigrations
或 migrate
中)是尽最大努力的承诺,而不是保证; 需要用于正确获取依赖项的任何其他应用程序都将是。
没有迁移的应用程序必须与有迁移的应用程序没有关系(ForeignKey
、ManyToManyField
等)。 有时它可能有效,但不受支持。
迁移文件
迁移以磁盘格式存储,此处称为“迁移文件”。 这些文件实际上只是普通的 Python 文件,具有商定的对象布局,以声明式编写。
基本迁移文件如下所示:
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [('migrations', '0001_initial')]
operations = [
migrations.DeleteModel('Tribble'),
migrations.AddField('Author', 'rating', models.IntegerField(default=0)),
]
Django 在加载迁移文件(作为 Python 模块)时寻找的是 django.db.migrations.Migration
的子类,称为 Migration
。 然后它检查这个对象的四个属性,大部分时间只使用其中的两个:
dependencies
,这个依赖的迁移列表。operations
,Operation
类的列表,用于定义此迁移的作用。
操作是关键; 它们是一组声明性指令,告诉 Django 需要进行哪些架构更改。 Django 扫描它们并为所有应用程序构建所有架构更改的内存表示,并使用它来生成使架构更改的 SQL。
该内存结构还用于计算模型与迁移的当前状态之间的差异; Django 在内存模型集上按顺序运行所有更改,以得出您上次运行 makemigrations
时模型的状态。 然后它使用这些模型与您的 models.py
文件中的模型进行比较,以找出您更改的内容。
您应该很少(如果有的话)需要手动编辑迁移文件,但如果需要,完全可以手动编写它们。 一些更复杂的操作无法自动检测,只能通过手写迁移使用,因此如果必须,请不要害怕编辑它们。
自定义字段
您不能在不引发 TypeError
的情况下修改已迁移的自定义字段中的位置参数的数量。 旧迁移将使用旧签名调用修改后的 __init__
方法。 因此,如果您需要一个新参数,请创建一个关键字参数并在构造函数中添加类似 assert 'argument_name' in kwargs
的内容。
模特经理
您可以选择将管理器序列化为迁移,并在 RunPython 操作中使用它们。 这是通过在管理器类上定义 use_in_migrations
属性来完成的:
class MyManager(models.Manager):
use_in_migrations = True
class MyModel(models.Model):
objects = MyManager()
如果您使用 from_queryset() 函数动态生成管理器类,则需要从生成的类继承以使其可导入:
class MyManager(MyBaseManager.from_queryset(CustomQuerySet)):
use_in_migrations = True
class MyModel(models.Model):
objects = MyManager()
请参阅有关迁移中的 历史模型 的注释,以了解随之而来的影响。
初始迁移
- Migration.initial
应用程序的“初始迁移”是创建该应用程序表的第一个版本的迁移。 通常一个应用程序只有一个初始迁移,但在某些复杂模型相互依赖的情况下,它可能有两个或更多。
初始迁移在迁移类上标有 initial = True
类属性。 如果未找到 initial
类属性,如果它是应用程序中的第一次迁移,则迁移将被视为“初始”(即 如果它不依赖于同一应用程序中的任何其他迁移)。
当使用 migrate --fake-initial
选项时,这些初始迁移会被特殊处理。 对于创建一个或多个表的初始迁移(CreateModel
操作),Django 会检查所有这些表是否已经存在于数据库中,如果存在则伪造应用迁移。 类似地,对于添加一个或多个字段的初始迁移(AddField
操作),Django 会检查所有相应的列是否已存在于数据库中,如果存在则伪造应用迁移。 如果没有 --fake-initial
,初始迁移的处理方式与任何其他迁移没有区别。
历史一致性
如前所述,当加入两个开发分支时,您可能需要手动线性化迁移。 在编辑迁移依赖项时,您可能会无意中创建不一致的历史状态,其中已应用迁移但其某些依赖项尚未应用。 这强烈表明依赖项不正确,因此 Django 将拒绝运行迁移或进行新的迁移,直到它被修复。 使用多个数据库时,可以使用 database routers 的 allow_migrate() 方法来控制哪些数据库 :djadmin:`makemigrations` 检查一致性历史记录。
向应用程序添加迁移
向新应用程序添加迁移很简单 - 它们预先配置为接受迁移,因此在您进行一些更改后只需运行 :djadmin:`makemigrations`。
如果您的应用程序已经有模型和数据库表,并且还没有迁移(例如,您是针对以前的 Django 版本创建的),则需要将其转换为使用迁移; 这是一个简单的过程:
$ python manage.py makemigrations your_app_label
这将为您的应用程序进行新的初始迁移。 现在,运行 python manage.py migrate --fake-initial
,Django 会检测到你有一个初始迁移 和 ,它想要创建的表已经存在,并将迁移标记为已经应用。 (如果没有 migrate --fake-initial
标志,命令会出错,因为它要创建的表已经存在。)
请注意,这仅适用于两件事:
- 自从您制作表格以来,您就没有更改过您的模型。 要使迁移工作,您必须首先进行初始迁移 ',然后进行更改,因为 Django 将更改与迁移文件而不是数据库进行比较。
- 您没有手动编辑您的数据库 - Django 将无法检测到您的数据库与您的模型不匹配,当迁移尝试修改这些表时,您只会收到错误。
恢复迁移
任何迁移都可以通过 :djadmin:`migrate` 使用以前的迁移数量来恢复:
$ python manage.py migrate books 0002
Operations to perform:
Target specific migration: 0002_auto, from books
Running migrations:
Rendering model states... DONE
Unapplying books.0003_auto... OK
如果要还原为应用程序应用的所有迁移,请使用名称 zero
:
$ python manage.py migrate books zero
Operations to perform:
Unapply all migrations: books
Running migrations:
Rendering model states... DONE
Unapplying books.0002_auto... OK
Unapplying books.0001_initial... OK
历史模型
当您运行迁移时,Django 正在使用存储在迁移文件中的模型的历史版本。 如果您使用 RunPython 操作编写 Python 代码,或者您的数据库路由器上有 allow_migrate
方法,则您 需要使用 这些历史模型版本而不是导入它们直接地。
警告
如果您直接导入模型而不是使用历史模型,您的迁移 最初可能会工作 ,但将来当您尝试重新运行旧迁移时(通常,当您设置新安装并运行时)将失败通过所有迁移来设置数据库)。
这意味着历史模型问题可能不会立即显现。 如果遇到这种故障,可以编辑迁移以使用历史模型而不是直接导入并提交这些更改。
因为不可能序列化任意 Python 代码,所以这些历史模型不会有您定义的任何自定义方法。 但是,它们将具有相同的字段、关系、管理器(仅限于具有 use_in_migrations = True
的那些)和 Meta
选项(也是版本化的,因此它们可能与您当前的不同)。
警告
这意味着在迁移中访问对象时,不会在对象上调用自定义 save()
方法,并且不会有任何自定义构造函数或实例方法。 合理规划!
字段选项中对函数的引用,例如 upload_to
和 limit_choices_to
以及具有 use_in_migrations = True
的管理器的模型管理器声明在迁移中被序列化,因此需要保留函数和类只要有迁移引用它们。 任何 自定义模型字段 也需要保留,因为这些是通过迁移直接导入的。
此外,模型的具体基类存储为指针,因此只要存在包含对它们的引用的迁移,就必须始终保留基类。 从好的方面来说,来自这些基类的方法和管理器通常会继承,因此如果您绝对需要访问这些,您可以选择将它们移动到超类中。
要删除旧引用,您可以 压缩迁移 ,或者,如果引用不多,请将它们复制到迁移文件中。
删除模型字段时的注意事项
与上一节中描述的“对历史函数的引用”注意事项类似,从您的项目或第三方应用程序中删除自定义模型字段,如果它们在旧迁移中被引用,则会导致问题。
为了帮助解决这种情况,Django 提供了一些模型字段属性来帮助使用 系统检查框架 来弃用模型字段。
将 system_check_deprecated_details
属性添加到您的模型字段,类似于以下内容:
class IPAddressField(Field):
system_check_deprecated_details = {
'msg': (
'IPAddressField has been deprecated. Support for it (except '
'in historical migrations) will be removed in Django 1.9.'
),
'hint': 'Use GenericIPAddressField instead.', # optional
'id': 'fields.W900', # pick a unique ID for your field.
}
在您选择的弃用期(Django 本身的字段的两个或三个功能版本)之后,将 system_check_deprecated_details
属性更改为 system_check_removed_details
并更新类似于以下内容的字典:
class IPAddressField(Field):
system_check_removed_details = {
'msg': (
'IPAddressField has been removed except for support in '
'historical migrations.'
),
'hint': 'Use GenericIPAddressField instead.',
'id': 'fields.E900', # pick a unique ID for your field.
}
您应该保留该字段在数据库迁移中运行所需的方法,例如 __init__()
、deconstruct()
和 get_internal_type()
。 只要存在引用该字段的任何迁移,就保留此存根字段。 例如,在压缩迁移并删除旧迁移后,您应该能够完全删除该字段。
数据迁移
除了更改数据库架构外,您还可以使用迁移来更改数据库本身中的数据,如果需要,还可以与架构结合使用。
改变数据的迁移通常称为“数据迁移”; 它们最好作为单独的迁移编写,与您的架构迁移一起使用。
Django 不能像模式迁移一样自动为你生成数据迁移,但编写它们并不难。 Django中的迁移文件由Operations组成,你用于数据迁移的主要操作是RunPython。
首先,创建一个您可以使用的空迁移文件(Django 会将文件放在正确的位置,建议一个名称,并为您添加依赖项):
python manage.py makemigrations --empty yourappname
然后,打开文件; 它应该是这样的:
# Generated by Django A.B on YYYY-MM-DD HH:MM
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
]
现在,您需要做的就是创建一个新函数并让 RunPython 使用它。 RunPython 需要一个 callable 作为其参数,它带有两个参数 - 第一个是 应用程序注册表 ,其中加载了所有模型的历史版本以匹配迁移历史中的位置坐下,第二个是 SchemaEditor,您可以使用它来手动影响数据库架构更改(但请注意,这样做可能会混淆迁移自动检测器!)
让我们编写一个简单的迁移,用 first_name
和 last_name
的组合值填充我们的新 name
字段(我们已经意识到并不是每个人都有第一个和姓氏)。 我们需要做的就是使用历史模型并遍历行:
from django.db import migrations
def combine_names(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Person = apps.get_model('yourappname', 'Person')
for person in Person.objects.all():
person.name = '%s %s' % (person.first_name, person.last_name)
person.save()
class Migration(migrations.Migration):
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
migrations.RunPython(combine_names),
]
完成后,我们可以正常运行 python manage.py migrate
,数据迁移将与其他迁移一起运行。
您可以将第二个可调用对象传递给 RunPython 以运行在向后迁移时要执行的任何逻辑。 如果省略此可调用对象,则向后迁移将引发异常。
从其他应用程序访问模型
当编写一个 RunPython
函数使用来自迁移所在应用程序以外的模型时,迁移的 dependencies
属性应包括所涉及的每个应用程序的最新迁移,否则您可能当您尝试使用 apps.get_model()
检索 RunPython
函数中的模型时,会收到类似于以下内容的错误:LookupError: No installed app with label 'myappname'
。
在以下示例中,我们在 app1
中有一个迁移,需要使用 app2
中的模型。 我们不关心 move_m1
的细节,只是它需要从两个应用程序访问模型。 因此,我们添加了一个依赖项,指定 app2
的最后一次迁移:
class Migration(migrations.Migration):
dependencies = [
('app1', '0001_initial'),
# added dependency to enable using models from app2 in move_m1
('app2', '0004_foobar'),
]
operations = [
migrations.RunPython(move_m1),
]
压缩迁移
我们鼓励您自由地进行迁移,而不必担心您有多少; 迁移代码经过优化,可以一次处理数百个而不会有太大的减速。 但是,最终您会希望从数百次迁移回到少数迁移,这就是压缩的用武之地。
Squashing 是将一组现有的许多迁移减少到一个(或有时是几个)迁移的行为,这些迁移仍然代表相同的更改。
Django 通过获取所有现有的迁移,提取它们的 Operation
并将它们按顺序排列,然后对它们运行优化器来尝试减少列表的长度来做到这一点 - 例如,它知道CreateModel和DeleteModel相互抵消,它知道AddField可以滚进CreateModel。
一旦尽可能减少操作序列 - 可能的数量取决于您的模型的交织程度以及您是否有任何 RunSQL 或 RunPython 操作(不能是除非它们被标记为 elidable
) - Django 然后会将其写回一组新的迁移文件。
这些文件被标记为表示它们替换了先前压缩的迁移,因此它们可以与旧的迁移文件共存,并且 Django 将根据您在历史记录中的位置智能地在它们之间切换。 如果您仍然在执行已压缩的迁移集,它将继续使用它们直到结束,然后切换到压缩的历史记录,而新安装将只使用新的压缩迁移并跳过所有旧的那些。
这使您能够压缩而不是搞乱当前生产中尚未完全更新的系统。 推荐的过程是压缩,保留旧文件,提交和发布,等到所有系统都升级到新版本(或者如果您是第三方项目,请确保您的用户按顺序升级版本,不要跳过任何一个) ,然后删除旧文件,提交并进行第二次发布。
支持这一切的命令是 :djadmin:`squashmigrations` - 只需将您想要压缩的应用程序标签和迁移名称传递给它,它就会开始工作:
$ ./manage.py squashmigrations myapp 0004
Will squash the following migrations:
- 0001_initial
- 0002_some_change
- 0003_another_change
- 0004_undo_something
Do you wish to proceed? [yN] y
Optimizing...
Optimized from 12 operations to 7 operations.
Created new squashed migration /home/andrew/Programs/DjangoTest/test/migrations/0001_squashed_0004_undo_somthing.py
You should commit this migration but leave the old ones in place;
the new migration will be used for new installs. Once you are sure
all instances of the codebase have applied the migrations you squashed,
you can delete them.
如果要设置压缩迁移的名称而不是使用自动生成的名称,请使用 squashmigrations --squashed-name
选项。
请注意,Django 中的模型相互依赖可能会变得非常复杂,并且压缩可能会导致无法运行的迁移; 要么优化不当(在这种情况下,您可以使用 --no-optimize
重试,但您也应该报告问题),或者使用 CircularDependencyError
,在这种情况下您可以手动解决它。
要手动解析 CircularDependencyError
,请将循环依赖循环中的一个外键分解为单独的迁移,并使用它移动对其他应用程序的依赖。 如果您不确定,请查看 :djadmin:`makemigrations` 如何处理被要求从您的模型创建全新迁移的问题。 在 Django 的未来版本中,:djadmin:`squashmigrations` 将更新以尝试自行解决这些错误。
压缩迁移后,您应该将其与它替换的迁移一起提交,并将此更改分发到应用程序的所有正在运行的实例,确保它们运行 migrate
以将更改存储在其数据库中。
然后,您必须通过以下方式将压缩迁移转换为正常迁移:
- 删除它替换的所有迁移文件。
- 将所有依赖于已删除迁移的迁移更新为依赖于压缩的迁移。
- 删除压缩迁移的
Migration
类中的replaces
属性(这是 Django 告诉它是压缩迁移的方式)。
笔记
压缩迁移后,在将其完全转换为正常迁移之前,不应重新压缩该压缩的迁移。
序列化值
迁移只是包含模型旧定义的 Python 文件 - 因此,要编写它们,Django 必须获取模型的当前状态并将它们序列化到一个文件中。
虽然 Django 可以序列化大多数东西,但有些东西我们不能序列化为有效的 Python 表示——没有关于如何将值转换回代码的 Python 标准(repr()
仅适用于基本的值,并且不指定导入路径)。
Django 可以序列化以下内容:
int
、float
、bool
、str
、bytes
、None
、NoneType
list
、set
、tuple
、dict
、range
。datetime.date
、datetime.time
和datetime.datetime
实例(包括时区感知的实例)decimal.Decimal
实例enum.Enum
实例uuid.UUID
实例functools.partial()
和functools.partialmethod
实例具有可序列化的func
、args
和keywords
值。LazyObject
包装可序列化值的实例。- 任何 Django 字段
- 任何函数或方法引用(例如
datetime.datetime.today
)(必须在模块的顶级范围内) - 在类体内使用的未绑定方法
- 任何类引用(必须在模块的顶级范围内)
- 任何带有自定义
deconstruct()
方法的东西( 见下文 )
2.1 版更改:添加了 对 functools.partialmethod
的序列化支持。
在 2.2 版更改:添加了 对 NoneType
的序列化支持。
Django 无法序列化:
- 嵌套类
- 任意类实例(例如
MyClass(4.3, 5.7)
) - 拉姆达
自定义序列化程序
2.2 版中的新功能。
您可以通过编写自定义序列化程序来序列化其他类型。 例如,如果 Django 默认没有序列化 Decimal
,你可以这样做:
from decimal import Decimal
from django.db.migrations.serializer import BaseSerializer
from django.db.migrations.writer import MigrationWriter
class DecimalSerializer(BaseSerializer):
def serialize(self):
return repr(self.value), {'from decimal import Decimal'}
MigrationWriter.register_serializer(Decimal, DecimalSerializer)
MigrationWriter.register_serializer()
的第一个参数是应该使用序列化程序的类型或可迭代的类型。
序列化程序的 serialize()
方法必须返回一个字符串,说明该值在迁移中的显示方式以及迁移中所需的一组导入。
添加 deconstruct() 方法
您可以通过为类提供 deconstruct()
方法来让 Django 序列化您自己的自定义类实例。 它不接受任何参数,并且应该返回一个包含三个元素的元组 (path, args, kwargs)
:
path
应该是类的 Python 路径,类名包含在最后一部分(例如,myapp.custom_things.MyClass
)。 如果您的类在模块的顶层不可用,则它不可序列化。args
应该是要传递给您的类的__init__
方法的位置参数列表。 此列表中的所有内容本身都应该是可序列化的。kwargs
应该是传递给您的类的__init__
方法的关键字参数的字典。 每个值本身都应该是可序列化的。
Django 将使用给定的参数将值写出作为您的类的实例,类似于它写出对 Django 字段的引用的方式。
为了防止每次运行 :djadmin:`makemigrations` 时都创建新的迁移,您还应该向装饰类添加一个 __eq__()
方法。 Django 的迁移框架会调用这个函数来检测状态之间的变化。
只要类的构造函数的所有参数本身都是可序列化的,您就可以使用 django.utils.deconstruct
中的 @deconstructible
类装饰器添加 deconstruct()
方法:
from django.utils.deconstruct import deconstructible
@deconstructible
class MyCustomClass:
def __init__(self, foo=1):
self.foo = foo
...
def __eq__(self, other):
return self.foo == other.foo
装饰器添加逻辑来捕获和保留传入构造函数的参数,然后在调用 deconstruct() 时准确地返回这些参数。
支持多个 Django 版本
如果您是带有模型的第三方应用程序的维护者,您可能需要提供支持多个 Django 版本的迁移。 在这种情况下,您应该始终使用您希望支持的最低 Django 版本 运行 :djadmin:`makemigrations` 。
迁移系统将根据与 Django 其余部分相同的策略保持向后兼容性,因此在 Django XY 上生成的迁移文件应在 Django X.Y+1 上保持不变。 然而,迁移系统不承诺向前兼容。 可能会添加新功能,并且使用较新版本的 Django 生成的迁移文件可能不适用于旧版本。