$ 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 会提示你,并给你一些选项。如果它认为足够安全,它将为你自动线性化两个迁移。如果不安全,你就得自己去修改迁移——别担心,这并不难,有关更多信息,请参见下面的 迁移文件。
在支持 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
中迁移的依赖。
这意味着当您运行迁移时,authors
迁移首先运行并创建了 ForeignKey
引用的表,然后创建 ForeignKey
列的迁移在其后运行并创建约束。如果没有这样的顺序,迁移将尝试在引用的表不存在的情况下创建 ForeignKey
列,这将导致数据库出错。
这种依赖性行为会影响大多数只限于单个应用的迁移操作。仅限于单个应用(无论是 makemigrations
还是 migrate
)是尽最大努力的承诺,而不是保证;任何其他需要用来正确获取依赖关系的应用程序都将是。
没有迁移的应用不得与有迁移的应用有关系(ForeignKey
,ManyToManyField
等)。有时可能可行,但不受支持。
可交换的依赖关系
django.db.migrations.
swappable_dependency
(value)
swappable_dependency()
函数在迁移中用于声明对被交换模型的应用中的迁移的 "可交换" 依赖关系,当前是对该应用的第一个迁移的依赖。因此,被交换模型应该在初始迁移中创建。参数 value
是一个描述应用标签和模型名称的字符串 "<app label>.<model>"
,例如 "myapp.MyModel"
。
使用 swappable_dependency()
,您通知迁移框架迁移依赖于另一个设置可交换模型的迁移,从而允许将来可能替换模型的不同实现。通常用于引用可能需要自定义或替换的模型,例如 Django 认证系统中的自定义用户模型(settings.AUTH_USER_MODEL
,默认为 "auth.User"
)。
迁移文件
迁移以磁盘格式存储,这里称为“迁移文件”。这些文件实际上是普通的 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
添加迁移。
如果您的应用程序已经拥有模型和数据库表,但尚未进行迁移(例如,您是在以前的 Django 版本中创建的),您需要通过运行以下命令将其转换为使用迁移:
$ python manage.py makemigrations your_app_label
这将为你的应用程序进行新的初始迁移。现在,运行 python manage.py migrate --fake-initial
,Django 将检测到你有一个初始迁移 并且 它要创建的表已经存在,而将迁移标记为已应用。(如果没有 migrate --fake-initial
标志,该命令将出错,因为它要创建的表已经存在。)
请注意,这只适用于以下两种情况:
自从你建立了表之后,你就没有改变过你的模型。要使迁移生效,你必须 首先 进行初始迁移,然后再进行更改,因为 Django 将变更与迁移文件(而不是数据库)进行比较。
你尚未手动编辑数据库——Django 无法检测到你的数据库与你的模型不匹配,当迁移尝试修改这些表时,你只会得到错误。
撤销迁移
可以通过 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
$ 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
方法,则你 需要使用 这些模型的历史版本而不是直接导入它们。
如果您直接导入模型而不是使用历史模型,您的迁移 可能在初始阶段工作,但在将来尝试重新运行旧迁移时将失败(通常是在设置新安装并运行所有迁移以设置数据库时)。
这意味着历史模型的问题可能不会立即显现。如果遇到这种故障,可以编辑迁移以使用历史模型,而不是直接导入并提交这些更改。
因为不可能序列化任意的 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
需要一个可调用对象作为它的参数,这个可调用对象需要两个参数——第一个是 应用注册表 ,其中加载了所有模型的历史版本,以匹配迁移所在的位置,第二个是 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 将根据你在历史记录中的位置智能地在它们之间切换。如果你仍处于压缩过程中,则它将继续使用它们直到结束,然后切换到压缩历史记录,而新安装将使用新压缩后的迁移并跳过所有旧迁移。
这样你就可以压缩而不至于把目前还没有完全更新的生产系统搞乱。推荐的流程是压缩,保留旧文件,提交并发布,等到所有系统都升级到新版本(或者如果你是第三方项目,确保你的用户按顺序升级版本,不跳过任何一个版本),然后删除旧文件,提交并进行第二次发布。
支持所有这些操作的命令是 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? [y/N] 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 告诉它是压缩迁移的方式)。
压缩迁移后,在完全将其转换为正常迁移之前,你不应该再重新压缩该压缩的迁移。
修剪已删除的迁移的引用
如果可能会在将来重复使用已删除的迁移的名称,您应该使用 migrate --prune
选项从 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
和 enum.Flag
实例
uuid.UUID
实例
functools.partial()
和具有可序列化 func
、args
和 keywords
值的 functools.partialmethod
实例。
来自 pathlib
的纯路径和具体路径对象。具体路径将被转换为其纯路径等效项,例如 pathlib.PosixPath
到 pathlib.PurePosixPath
。
os.PathLike
实例,例如 os.DirEntry
,可以使用 os.fspath()
转换为 str
或 bytes
。
包含可序列化值的 LazyObject
实例。
枚举类型(例如 TextChoices
或 IntegerChoices
)实例。
任何 Django 字段
任何函数或方法引用(如 datetime.datetime.today
)(必须在模块的顶层范围内)