迁移是 Django 将你对模型的修改(例如增加一个字段,删除一个模型)应用至数据库架构中的方式。它们被设计的尽可能自动化,但你仍需要知道何时构建和运行迁移,你还需要了解一些常见问题。
以下是几个常用的与迁移交互的命令,即 Django 处理数据库架构的方式:
migrate
,负责应用和撤销迁移。makemigrations
,基于模型的修改创建迁移。sqlmigrate
,展示迁移使用的 SQL 语句。showmigrations
,列出项目的迁移和迁移的状态。你应该将迁移看作是数据库架构的版本控制系统。 makemigrations
负责将模型修改打包进独立的迁移文件中——类似提交修改,而 migrate
负责将其应用至数据库。
每个应用的迁移文件位于该应用的 "migrations" 目录中,他们被设计成应用代码的一部分,与应用代码一起被提交,被发布。你只需在开发机上构建一次,就可以在同事的电脑或测试机上运行同样的迁移而保证结果一致。最后在生产环境运行同样的迁移。
备注
通过修改配置 MIGRATION_MODULES
可以重写包含迁移的应用的包名。
从同样的数据集合运行迁移在开发、测试和生产环境都会生成同样的结果。
Django 会在修改模型或字段时生成迁移——即便修改的是不会影响数据库的配置——因为唯一能确保结果正确性的方法时完整记录修改历史,而且这些东西你以后可能在某些数据迁移中用的到(例如,已设置了自定义验证器的时候)。
所有 Django 支持的数据库后端都支持迁移,还有些支持表修改(通过 SchemaEditor 类实现)的第三方后端也支持。
然而,有些数据库在表结构变更方面比其它数据库更强;下面介绍一些注意事项。
PostgreSQL 在架构支持方面是所有数据库中是最强的。
MySQL 缺乏对架构变更操作相关事务的支持,这意味着如果迁移失败,你将必须手动取消更改才能重试(无法回滚到较早的时间)。
此外,MySQL 几乎每一次架构操作都会完全重写表,一般来说,增加或删除列需要的时间与表的行数成正比。在速度较慢的硬件上,这可能比每百万行一分钟还要糟糕——在一个只有几百万行的表中添加几列,可能会让你的网站锁定十几分钟。
最后,MySQL 对列、表和索引的名称长度有相对较小的限制,并且对索引涵盖的所有列的组合大小也有限制。 这意味着在其他后端上创建的索引将可能无法在 MySQL 下创建。
SQLite 几乎没有内置的架构更改支持,因此 Django 尝试通过以下方式对其进行模拟:
此过程一般工作的很好,但它可能很慢,偶尔也会出现问题。除非你非常清楚风险和它的局限性,否则不建议你在生产环境中运行和迁移 SQLite;Django 自带的支持是为了让开发人员在本地计算机上使用 SQLite 来开发较不复杂的 Django 项目,而无需完整的数据库。
Django can create migrations for you. Make changes to your models - say, add a
field and remove a model - and then run makemigrations
:
$ python manage.py makemigrations
Migrations for 'books':
books/migrations/0003_auto.py:
- Alter field author on book
你的模型将被扫描并与当前包含在你的迁移文件中的版本进行比较,然后将写出一组新的迁移。请务必阅读输出,看看 makemigrations
认为你已更改的内容——它并不完美,对于复杂的更改,可能无法检测到你所期望的。
Once you have your new migration files, you should apply them to your database to make sure they work as expected:
$ python manage.py migrate
Operations to perform:
Apply all migrations: books
Running migrations:
Rendering model states... DONE
Applying books.0003_auto... OK
一旦应用了迁移,将迁移和模型更改作为一个单一的提交来提交到您的版本控制系统——这样,当其他开发人员(或你的生产服务器)检查代码时,他们将同时获得对你的模型的更改和伴随的迁移。
If you want to give the migration(s) a meaningful name instead of a generated
one, you can use the makemigrations --name
option:
$ python manage.py makemigrations --name changed_my_model your_app_label
在支持 DDL 事务的数据库上(SQLite 和 PostgreSQL),所有的迁移操作默认都会在一个事务中运行。相反,如果一个数据库不支持 DDL 事务(如 MySQL、Oracle),那么所有的操作将在没有事务的情况下运行。
你可以通过将 atomic
属性设置为 False
来防止迁移在事务中运行。例如:
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
也可以使用 atomic()
或者通过传递 atomic=True
到 RunPython
来在事务中执行部分迁移。更多细节请参见 非原子性迁移。
虽然迁移是按应用进行的,但你的模型所隐含的表和关系太复杂,不可能同时为一个应用创建。当你进行迁移时,需要运行其他的东西——例如,你在你的 books
应用中添加了一个指向 authors
应用的 ForeignKey
——最终的迁移将包含对 authors
中迁移的依赖。
This means that when you run the migrations, the authors
migration runs
first and creates the table the ForeignKey
references, and then the migration
that makes the ForeignKey
column runs afterward and creates the constraint.
If this didn't happen, the migration would try to create the ForeignKey
column without the table it's referencing existing and your database would
throw an error.
这种依赖性行为会影响大多数只限于单个应用的迁移操作。仅限于单个应用(无论是 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
操作中使用它们。这是通过在 manager 类上定义一个 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()
方法来控制 makemigrations
检查哪些数据库的历史一致。
新的应用已预先配置为接受迁移,因此你可以在进行一些更改后通过运行 makemigrations
添加迁移。
If your app already has models and database tables, and doesn't have migrations yet (for example, you created it against a previous Django version), you'll need to convert it to use migrations by running:
$ python manage.py makemigrations your_app_label
这将为你的应用程序进行新的初始迁移。现在,运行 python manage.py migrate --fake-initial
,Django 将检测到你有一个初始迁移 并且 它要创建的表已经存在,而将迁移标记为已应用。(如果没有 migrate --fake-initial
标志,该命令将出错,因为它要创建的表已经存在。)
请注意,这只适用于以下两种情况:
可以通过 migrate
传递上一次迁移的编号来撤销迁移。例如,要撤销迁移 books.0003
:
$ 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
...\> py 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
...\> py 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
如果迁移包含任何不可逆的操作,则该迁移是不可逆的。 试图撤销这种迁移将引发 IrreversibleError
:
$ 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...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL sql='DROP TABLE demo_books'> in books.0003_auto is not reversible
...\> py 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...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL sql='DROP TABLE demo_books'> in books.0003_auto is not reversible
当你运行迁移时,Django 正在使用存储在迁移文件中的模型的历史版本。如果你使用 RunPython
操作编写 Python 代码,或者你的数据库路由上有 allow_migrate
方法,则你 需要使用 这些模型的历史版本而不是直接导入它们。
警告
If you import models directly rather than using the historical models, your migrations may work initially but will fail in the future when you try to rerun old migrations (commonly, when you set up a new installation and run through all the migrations to set up the database).
这意味着历史模型的问题可能不会立即显现。如果遇到这种故障,可以编辑迁移以使用历史模型,而不是直接导入并提交这些更改。
因为不可能序列化任意的 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
。
To start, make an empty migration file you can work from (Django will put the file in the right place, suggest a name, and add dependencies for you):
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
需要一个可调用对象作为它的参数,这个可调用对象需要两个参数——第一个是 应用注册表 ,其中加载了所有模型的历史版本,以匹配迁移所在的位置,第二个是 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 = f"{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),
]
我们鼓励你自由地进行迁移,而不要担心你有多少迁移;迁移代码经过优化,可以一次处理几百个迁移,而不会有太多的减速。然而,最终你会希望从几百个迁移回归到只有几个,这就是压缩的作用。
压缩是将一组现有的多个迁移减少到一个(有时是几个)迁移,这些迁移仍然代表相同的更改。
Django通过获取所有现有迁移,提取它们的 Operation
并将它们按顺序排列,然后对它们运行一个优化器,以尝试减少列表的长度——例如,它知道 CreateModel
和 DeleteModel
相互抵消,它还知道 AddField
可以卷入 CreateModel
。
一旦操作序列被尽可能地减少——可能的数量取决于你的模型有多紧密交织,如果你有任何 RunSQL
或 RunPython
操作(除非它们被标记为 elidable
,否则无法被优化),Django就会把它写回一组新的迁移文件中。
这些文件被标记为替换了先前压缩的迁移,因此它们可以与旧迁移文件共存,Django 将根据你在历史记录中的位置智能地在它们之间切换。如果你仍处于压缩过程中,则它将继续使用它们直到结束,然后切换到压缩历史记录,而新安装将使用新压缩后的迁移并跳过所有旧迁移。
这样你就可以压缩而不至于把目前还没有完全更新的生产系统搞乱。推荐的流程是压缩,保留旧文件,提交并发布,等到所有系统都升级到新版本(或者如果你是第三方项目,确保你的用户按顺序升级版本,不跳过任何一个版本),然后删除旧文件,提交并进行第二次发布。
The command that backs all this is squashmigrations
- pass it the
app label and migration name you want to squash up to, and it'll get to work:
$ ./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_something.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
问题,请将循环依赖中的外键分离到单独的迁移中,并将依赖项移到另一个应用上。如果你不确定,请参见 makemigrations
在被要求从模型创建全新的迁移时如何处理问题。在未来的 Django 版本中,squashmigrations
将被更新以尝试自己解决这些错误。
一旦你压缩了你的迁移,你应该把它和它所替代的迁移一起提交,并把这个更改分发到你的应用程序的所有运行中的实例,确保它们运行 migrate
来将更改存储在它们的数据库中。
然后,你必须通过以下方法将压缩的迁移过渡到正常迁移:
Migration
类的 replaces
属性(这就是 Django 告诉它是压缩迁移的方式)。备注
压缩迁移后,在完全将其转换为正常迁移之前,你不应该再重新压缩该压缩的迁移。
Pruning references to deleted migrations
If it is likely that you may reuse the name of a deleted migration in the
future, you should remove references to it from Django’s migrations table
with the migrate --prune
option.
迁移是包含模型旧定义的 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
and enum.Flag
instancesuuid.UUID
实例functools.partial()
和具有可序列化 func
、args
和 keywords
值的 functools.partialmethod
实例。pathlib
. Concrete paths are
converted to their pure path equivalent, e.g. pathlib.PosixPath
to
pathlib.PurePosixPath
.os.PathLike
instances, e.g. os.DirEntry
, which are
converted to str
or bytes
using os.fspath()
.LazyObject
实例。TextChoices
或 IntegerChoices
)实例。datetime.datetime.today
)(必须在模块的顶层范围内)deconstruct()
方法的任何东西(见下文)Serialization support for enum.Flag
was added.
Django 不能序列化:
MyClass(4.3, 5.7)
)你可以通过编写一个自定义的序列化器来序列化其他类型。例如,如果 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__
方法。每个值本身应该是可序列化的。备注
此返回值与 自定义字段 的 deconstruct()
方法不同,后者返回四个项组成的元组。
Django 会用给定的参数将值作为你的类的实例化写出来,类似于它写出对 Django 字段的引用的方式。
为了防止每次运行 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版本 运行 makemigrations
。
迁移系统会按照与 Django 其他部分相同的策略保持向后兼容,所以在 Django X.Y 上生成的迁移文件在 Django X.Y+1 上运行时应该没有变化。但是,迁移系统并不保证向前兼容。新的功能可能会被添加,而且用新版本的 Django 生成的迁移文件可能无法在旧版本上运行。
5月 31, 2023