迁移

迁移是 Django 将你对模型的修改(例如增加一个字段,删除一个模型)应用至数据库架构中的方式。它们被设计的尽可能自动化,但你仍需要知道何时构建和运行迁移,你还需要了解一些常见问题。

命令

以下是几个常用的与迁移交互的命令,即 Django 处理数据库架构的方式:

你应该将迁移看作是数据库架构的版本控制系统。 makemigrations 负责将模型修改打包进独立的迁移文件中——类似提交修改,而 migrate 负责将其应用至数据库。

每个应用的迁移文件位于该应用的 "migrations" 目录中,他们被设计成应用代码的一部分,与应用代码一起被提交,被发布。你只需在开发机上构建一次,就可以在同事的电脑或测试机上运行同样的迁移而保证结果一致。最后在生产环境运行同样的迁移。

注解

通过修改配置 MIGRATION_MODULES 可以重写包含迁移的应用的包名。

从同样的数据集合运行迁移在开发、测试和生产环境都会生成同样的结果。

Django 会在修改模型或字段时生成迁移——即便修改的是不会影响数据库的配置——因为唯一能确保结果正确性的方法时完整记录修改历史,而且这些东西你以后可能在某些数据迁移中用的到(例如,已设置了自定义验证器的时候)。

后端支持

所有 Django 支持的数据库后端都支持迁移,还有些支持表修改(通过 SchemaEditor 类实现)的第三方后端也支持。

然而,有些数据库在表结构变更方面比其它数据库更强;下面介绍一些注意事项。

PostgreSQL

PostgreSQL 在架构支持方面是所有数据库中是最强的。

唯一需要注意的是,在 PostgreSQL 11 之前,添加具有默认值的列会导致表的完全重写,时间长短与表的大小成正比。 因此,建议你始终使用 null=True 创建新列,因为这样可以立即添加它们。

MySQL

MySQL 缺乏对架构变更操作相关事务的支持,这意味着如果迁移失败,你将必须手动取消更改才能重试(无法回滚到较早的时间)。

此外,MySQL 几乎每一次架构操作都会完全重写表,一般来说,增加或删除列需要的时间与表的行数成正比。在速度较慢的硬件上,这可能比每百万行一分钟还要糟糕——在一个只有几百万行的表中添加几列,可能会让你的网站锁定十几分钟。

最后,MySQL 对列、表和索引的名称长度有相对较小的限制,并且对索引涵盖的所有列的组合大小也有限制。 这意味着在其他后端上创建的索引将可能无法在 MySQL 下创建。

SQLite

SQLite 几乎没有内置的架构更改支持,因此 Django 尝试通过以下方式对其进行模拟:

  • 使用新架构创建新表
  • 复制数据
  • 删除旧表
  • 重新命名新表,使之与原表名相匹配。

此过程一般工作的很好,但它可能很慢,偶尔也会出现问题。除非你非常清楚风险和它的局限性,否则不建议你在生产环境中运行和迁移 SQLite;Django 自带的支持是为了让开发人员在本地计算机上使用 SQLite 来开发较不复杂的 Django 项目,而无需完整的数据库。

工作流程

Django 可以为你创建迁移。对你的模型进行修改——比如说,添加一个字段和删除一个模型——然后运行 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 会提示你,并给你一些选项。如果它认为足够安全,它将为你自动线性化两个迁移。如果不安全,你就得自己去修改迁移——别担心,这并不难,有关更多信息,请参见下面的 迁移文件

事务

在支持 DDL 事务的数据库上(SQLite 和 PostgreSQL),所有的迁移操作默认都会在一个事务中运行。相反,如果一个数据库不支持 DDL 事务(如 MySQL、Oracle),那么所有的操作将在没有事务的情况下运行。

你可以通过将 atomic 属性设置为 False 来防止迁移在事务中运行。例如:

from django.db import migrations

class Migration(migrations.Migration):
    atomic = False

也可以使用 atomic() 或者通过传递 atomic=TrueRunPython 来在事务中执行部分迁移。更多细节请参见 非原子性迁移

依赖

虽然迁移是按应用进行的,但你的模型所隐含的表和关系太复杂,不可能同时为一个应用创建。当你进行迁移时,需要运行其他的东西——例如,你在你的 books 应用中添加了一个指向 authors 应用的 ForeignKey——最终的迁移将包含对 authors 中迁移的依赖。

这意味着当你运行迁移时,authors 迁移将首先运行并创建 ForeignKey 所引用的表,然后构成 ForeignKey 列的迁移将随后运行并创建约束。如果不这样做,迁移将尝试在它所引用的表不存在的情况下创建 ForeignKey 列,你的数据库将抛出一个错误。

这种依赖性行为会影响大多数只限于单个应用的迁移操作。仅限于单个应用(无论是 makemigrations 还是 migrate)是尽最大努力的承诺,而不是保证;任何其他需要用来正确获取依赖关系的应用程序都将是。

没有迁移的应用不得与有迁移的应用有关系(ForeignKeyManyToManyField 等)。有时可能可行,但不受支持。

迁移文件

迁移以磁盘格式存储,这里称为“迁移文件”。这些文件实际上是普通的 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 routersallow_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
...\> 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 方法,则你 需要使用 这些模型的历史版本而不是直接导入它们。

警告

如果你直接导入模型而不是使用历史模型,则迁移 最初可能会工作 但将来在尝试重新运行旧迁移时会失败(通常,当你设置新安装并运行所有迁移时 以建立数据库时)。

这意味着历史模型的问题可能不会立即显现。如果遇到这种故障,可以编辑迁移以使用历史模型,而不是直接导入并提交这些更改。

因为不可能序列化任意的 Python 代码,这些历史模型不会有你定义的任何自定义方法。然而,它们将具有相同的字段、关系、管理器(仅限于那些具有 use_in_migrations = True)和 Meta 选项(也有版本控制,因此它们可能与当前的不同)。

警告

这意味着在迁移中访问对象时,将不会对对象调用自定义的 save() 方法,也不会有任何自定义构造函数或实例方法。适当的计划一下吧!

字段选项中对函数的引用,例如 upload_tolimit_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_namelast_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),
    ]

更高级的迁移

如果你对更高级的迁移操作感兴趣,或者希望能够编写自己的迁移操作,请参阅 迁移操作参考 和“如何” 编写操作

压缩迁移

我们鼓励你自由地进行迁移,而不要担心你有多少迁移;迁移代码经过优化,可以一次处理几百个迁移,而不会有太多的减速。然而,最终你会希望从几百个迁移回归到只有几个,这就是压缩的作用。

压缩是将一组现有的多个迁移减少到一个(有时是几个)迁移,这些迁移仍然代表相同的更改。

Django通过获取所有现有迁移,提取它们的 Operation 并将它们按顺序排列,然后对它们运行一个优化器,以尝试减少列表的长度——例如,它知道 CreateModelDeleteModel 相互抵消,它还知道 AddField 可以卷入 CreateModel

一旦操作序列被尽可能地减少——可能的数量取决于你的模型有多紧密交织,如果你有任何 RunSQLRunPython 操作(除非它们被标记为 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? [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 问题,请将循环依赖中的外键分离到单独的迁移中,并将依赖项移到另一个应用上。如果你不确定,请参见 makemigrations 在被要求从模型创建全新的迁移时如何处理问题。在未来的 Django 版本中,squashmigrations 将被更新以尝试自己解决这些错误。

一旦你压缩了你的迁移,你应该把它和它所替代的迁移一起提交,并把这个更改分发到你的应用程序的所有运行中的实例,确保它们运行 migrate 来将更改存储在它们的数据库中。

然后,你必须通过以下方法将压缩的迁移过渡到正常迁移:

  • 删除它替换的所有迁移文件。
  • 将所有依赖被删除迁移的迁移更新为依赖被压缩的迁移。
  • 删除压缩迁移的 Migration 类的 replaces 属性(这就是 Django 告诉它是压缩迁移的方式)。

注解

压缩迁移后,在完全将其转换为正常迁移之前,你不应该再重新压缩该压缩的迁移。

序列化值

迁移是包含模型旧定义的 Python 文件,因此,要编写它们,Django 必须获取模型的当前状态并将它们序列化到一个文件中。

虽然 Django 可以序列化大多数内容,但有些内容我们无法序列化为有效的 Python 表示形式——对于如何将值转换回代码,没有 Python 标准(repr() 只适用于基本的值,而且没有指定导入路径)。

Django 可以序列化以下内容:

  • intfloatboolstrbytesNoneNoneType
  • listsettupledictrange
  • datetime.datedatetime.timedatetime.datetime 实例(包括可识别时区的实例)
  • decimal.Decimal 实例
  • enum.Enum 实例
  • uuid.UUID 实例
  • functools.partial() 和具有可序列化 funcargskeywords 值的 functools.partialmethod 实例。
  • Pure and concrete path objects from 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 实例。
  • 枚举类型(例如 TextChoicesIntegerChoices)实例。
  • 任何 Django 字段
  • 任何函数或方法引用(如 datetime.datetime.today)(必须在模块的顶层范围内)
  • 在类主体内部使用的未绑定方法
  • 任何类引用(必须在模块的顶层范围内)
  • 具有自定义 deconstruct() 方法的任何东西(见下文
Changed in Django 3.2:

Serialization support for pure and concrete path objects from pathlib, and os.PathLike instances 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 版本的迁移。在这种情况下,你应该始终 使用你希望支持的最低Django版本 运行 makemigrations

迁移系统会按照与 Django 其他部分相同的策略保持向后兼容,所以在 Django X.Y 上生成的迁移文件在 Django X.Y+1 上运行时应该没有变化。但是,迁移系统并不保证向前兼容。新的功能可能会被添加,而且用新版本的 Django 生成的迁移文件可能无法在旧版本上运行。

参见

迁移操作参考
涵盖架构操作 API,特殊操作以及编写自己的操作。
编写迁移的“方法”
介绍如何为你可能遇到的不同情况构建和编写数据库迁移。
Back to Top