迁移

迁移是Django将模型的更改(添加字段、删除模型等)同步到数据库模式的方式 的方式。 它的设计是很智能的, 但是你还是需要了解什么时候进行迁移、什么时候去运行它们、以及可能遇到的常见问题。

命令

有几个命令将用于迁移和Django对数据库模式进行操作:

你可以想象 migrations相当一个你的数据库的一个版本控制系统。 makemigrations 命令负责保存你的模型变化到一个迁移文件 - 和 commits很类似 - 同时 migrate负责将改变提交到数据库。

每个app 的迁移文件会保存到每个相应app的“migrations”文件夹里面,并且准备如何去执行它, 作为一个分布式代码库。 每当在你的开发机器或是你同事的机器并且最终在你的生产机器上运行同样的迁移,你应当再创建这些文件。

通过修改 MIGRATION_MODULES 设置,可以覆盖那些每个app 都包含 migrations 的 package

同样的方式,同样的数据集,将产生一致的结果, 那意味着你在开发和筹划中在按照同样的原理运行的情况下得到的结果和线上是一致的

Django 将迁移你对模型和字段做出的任何改变 - 甚至包括那些对数据库没有影响的操作, -在历史记录存放所有改变是正确重建field的唯一方式,你可能会在以后的数据迁移中使用到这些选项 (比如,在你定制了校验器的时候)。

后端支持

Django附带的所有后端都支持迁移,以及任何第三方后端,如果他们已被编程支持模式更改(通过SchemaEditor类完成)。

然而,有些数据库在架构迁移方面比其他数据库更有能力;下面有一些注意事项。

PostgreSQL

PostgreSQL是支持模式的最有能力的所有数据库;唯一需要注意的是,使用默认值添加列将导致表的完全重写,与其大小成正比。

因此,建议您始终使用null=True创建新列,因为这样会立即添加。

MySQL

MySQL缺少对模式更改操作的事务的支持,这意味着如果迁移无法应用,则必须手动取消批准更改才能再次尝试(不可能回滚到更早的点)。

此外,MySQL将为每个模式操作完全重写表,通常需要与表中的行数成正比的时间来添加或删除列。 在较慢的硬件上,这可能比每分钟每百万行更糟 - 向表中添加几列只有几百万行可能会锁定您的网站超过十分钟。

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

SQLite

SQLite具有非常少的内置模式更改支持,因此Django尝试通过以下方式模拟它:

  • 使用新模式创建新表
  • 复制数据
  • 删除旧表
  • 重命名新表以匹配原始名称

这个过程一般效果很好,但它可能很慢,偶尔会出现问题。 不建议您在生产环境中运行和迁移SQLite,除非您非常了解风险及其限制; Django支持的支持旨在允许开发人员在本地计算机上使用SQLite来开发不太复杂的Django项目,而不需要完整的数据库。

工作流¶ T0>

迁移工作很简单。 修改你的模型 -比如添加字段和移除一个模型 - 然后运行 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会提示你并给你一些选项。 如果它认为它足够安全,它会为你自动线性化两个迁移。 如果没有,您必须自行修改迁移作业,别担心,这并不难,在下面的Migration files中会有更多说明。

依赖

虽然迁移是基于应用程序的,但是模型所包含的表和关系太复杂,无法一次为一个应用程序创建。 当您进行迁移时,需要其他东西来运行——例如,您可以在authors应用添加一个ForeignKey到您的authors应用——由此产生的迁移将包含对books的迁移的依赖

这意味着当您运行迁移时,authors迁移首先运行,创建ForeignKey引用的表,然后进行ForeignKey 如果没有发生这种情况,迁移将尝试创建ForeignKey列,而不引用现有的表,并且您的数据库会抛出错误。

此依赖关系行为会影响大多数迁移操作,您可以限制一个应用程序。 限制一个应用程序(在makemigrationsmigrate)是一个尽力而为的承诺,而不是保证;任何其他需要用于获取依赖关系的应用程序将是。

迁移文件

迁移存储为磁盘格式,此处称为“迁移文件”。 这些文件实际上只是正常的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)),
    ]

加载迁移文件(作为Python模块)时,Django查找的是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()

请参阅迁移中有关Historical models的注意事项,了解其中的影响。

初始迁移

Migration.initial

应用程序的“初始迁移”是创建该应用程序表的第一个版本的迁移。 通常一个应用程序将只有一个初始迁移,但在某些情况下,复杂的模型相互依赖关系可能有两个或更多。

初始迁移在迁移类上标有初始化 = True类属性。 如果没有找到initial类属性,迁移将被视为“初始”,如果它是应用程序中的第一个迁移(即,如果它不依赖于同一个应用程序中的任何其他迁移) 。

当使用migrate --fake-initial选项时,将特别对这些初始迁移进行处理。 对于创建一个或多个表(CreateModel操作)的初始迁移,Django检查所有这些表已经存在于数据库中并假冒 - 如果是,则应用迁移。 类似地,对于添加一个或多个字段(AddField操作)的初始迁移,Django会检查数据库中是否已经存在所有相应的列,并且假如应用迁移。 没有--fake-initial,初始迁移与任何其他迁移的处理方式不同。

历史一致性

如前所述,您可能需要在连接两个开发分支时手动线性化迁移。 在编辑迁移依赖关系时,您可能会无意中创建历史记录不一致的状态,其中已经应用迁移,但是其中的某些依赖关系尚未被应用。 这是一个强有力的迹象表明依赖关系是不正确的,所以Django将拒绝运行迁移或进行新的迁移,直到它被修复。 使用多个数据库时,您可以使用database routersallow_migrate()方法来控制哪些数据库makemigrations检查一致的历史记录。

在Django更改1.10:

添加了迁移一致性检查。 1.10.1中添加了基于数据库路由器的检查。

将迁移添加到应用程序

将迁移添加到新应用程序很简单 - 它们已预先配置为接受迁移,因此,一旦进行了某些更改,只需运行makemigrations即可。

如果您的应用程序已经有模型和数据库表,并且还没有进行迁移(例如,您已经根据之前的Django版本创建了它),则需要将其转换为使用迁移;这是一个简单的过程:

$ python manage.py makemigrations your_app_label

这将为您的应用程序进行新的初始迁移。 现在,运行python manage.py migrate - fake-initial Django将检测到您有初始迁移表示它想要创建的表已经存在,并且将标记已经应用的迁移。 (没有migrate --fake-initial标志,该命令将出错,因为它想要创建的表已经存在。

注意,这只工作给予两件事:

  • 你没有改变你的模型,因为你制作了他们的表。 要使迁移正常工作,必须先进行初始迁移,然后进行更改,因为Django会将更改与迁移文件进行比较,而不是数据库。
  • 您没有手动编辑数据库 - Django将无法检测到您的数据库与您的模型不匹配,您只会在迁移尝试修改这些表时收到错误。

历史模型

当您运行迁移时,Django使用存储在迁移文件中的模型的历史版本。 如果您使用RunPython操作编写Python代码,或者如果在数据库路由器上使用allow_migrate方法,则会暴露给这些版本的模型。

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

警告

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

引用字段选项(例如upload_tolimit_choices_to中的函数以及管理员具有use_in_migrations = t6 > True在迁移中进行序列化,因此只要有迁移引用它们,函数和类就需要保留。 还需要保留任何custom model fields,因为这些是通过迁移直接导入的。

此外,模型的基类只存储为指针,因此只要存在包含对它们的引用的迁移,就必须始终保持基类。 在正面,这些基类的方法和管理器通常继承,所以如果你绝对需要访问这些,你可以选择将它们移动到超类。

要删除旧引用,您可以squash migrations,或者如果没有很多引用,请将其复制到迁移文件中。

删除模型字段时的注意事项

与上一节中介绍的“对历史函数的引用”注意事项类似,如果在旧迁移中引用了自定义模型字段,则从项目或第三方应用中移除自定义模型字段将会导致问题。

为了帮助解决这种情况,Django提供了一些模型字段属性,以帮助使用system checks framework来取消模型字段。

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__()get_internal_type()deconstruct() 只要引用此字段的任何迁移存在,就保留此存根字段。 例如,在压缩迁移并删除旧迁移后,您应该可以完全删除该字段。

数据迁移

除了更改数据库模式之外,您还可以使用迁移来更改数据库中的数据,如果需要,还可以与模式一起使用。

改变数据的迁移通常称为“数据迁移”;它们最好写成单独的迁移,与您的模式迁移一起。

Django不能为你自动生成数据迁移,就像它对模式迁移一样,但是写它们并不困难。 Django中的迁移文件由Operations组成,用于数据迁移的主要操作是RunPython

首先,创建一个你可以工作的空的迁移文件,(Django会把文件放在正确的地方,建议一个名字,并为你添加依赖关系):

python manage.py makemigrations --empty yourappname

然后打开文件;它应该看起来像这样:

# -*- coding: utf-8 -*-
# Generated by Django A.B on YYYY-MM-DD HH:MM
from __future__ import unicode_literals

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('yourappname', '0001_initial'),
    ]

    operations = [
    ]

现在,所有你需要做的是创建一个新函数,并使用RunPython RunPython需要一个可调用作为其参数,它需要两个参数 - 第一个是一个app registry,它将所有模型的历史版本加载到其中以匹配历史记录迁移就位,第二个是SchemaEditor,您可以使用它手动实现数据库模式更改(但要小心,这样做可能会使迁移自动检测器混乱)。

让我们编写一个简单的迁移,用namelast_name的组合值填充新的first_name字段(我们来到了我们的感觉不是每个人都有名和姓)。 所有我们需要做的是使用历史模型并在行上进行迭代:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

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属性应包括涉及的每个应用程序的最新迁移,否则可能会收到类似于以下错误的错误: LookupError: 没有 安装 应用 标签 'myappname' 当您尝试使用apps.get_model()检索RunPython函数中的模型时。

在下面的示例中,我们在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),
    ]

更高级的迁移

如果你对更高级的迁移操作感兴趣, 或者想写自定义迁移文件, see the migration operations reference and the “how-to” on writing migrations.

压缩迁移

鼓励你自由迁移,不要担心你有多少;迁移代码被优化为一次处理数百次而没有太多的减速。 然而,最终你会想从几百个迁移回到几个,这需要挤压。

压缩是将现有的许多迁移集减少到仍然表示相同更改的一个(或有时几个)迁移的行为。

Django通过采取所有现有迁移,提取其Operation并将它们全部顺序,然后在它们上运行优化器来尝试并减少列表的长度,这样做 - 例如,它知道CreateModelDeleteModel互相取消,并且知道AddField可以滚动到CreateModel

一旦操作顺序尽可能地减少 - 可能的数量取决于您的模型是如何密切相关的,如果您有任何RunSQLRunPython操作(可以'除非它们被标记为elidable),否则将进行优化。 - Django将会将其写回一组新的迁移文件。

这些文件被标记为说它们替换以前被压缩的迁移,因此它们可以与旧的迁移文件共存,并且Django将根据您在历史记录中的位置智能地在它们之间切换。 如果您仍然部分地通过您压缩的迁移集,它将继续使用它们,直到它到达结束,然后切换到压缩的历史记录,而新的安装将只使用新的压缩迁移,并跳过所有的旧那些。

这使您可以压缩,而不是混乱当前生产中尚未完全更新的系统。 建议的过程是压缩,保留旧文件,提交和释放,等待所有系统升级到新版本(或如果您是第三方项目,只是确保您的用户升级版本,而不跳过任何) ,然后删除旧的文件,提交并做第二个版本。

支持所有这一切的命令是squashmigrations - 只要将app标签和你想要压缩的迁移名称传递给它,它就会工作:

$ ./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.

请注意,Django中的模型相互依赖可能会变得非常复杂,挤压可能导致不运行的迁移;错误优化(在这种情况下,您可以尝试使用--no-optimize,尽管您还应该报告问题),或使用CircularDependencyError,在这种情况下您可以手动解决它。

要手动解析CircularDependencyError,请将循环依赖关系循环中的一个ForeignKeys拆分为单独的迁移,并使用它移动其他应用程序的依赖关系。 如果您不确定,请问当您从模型中创建全新的迁移时,makemigrations如何处理这个问题。 在将来的Django版本中,将会更新squashmigrations以尝试自己解决这些错误。

一旦您压缩了迁移,您应该将其替换为它替换的迁移,并将此更改分发到应用程序的所有正在运行的实例,确保它们运行migrate以将更改存储在其数据库中。

然后,您必须通过以下方式将挤压的迁移过渡到正常迁移:

  • 删除其所有的迁移文件。
  • 更新依赖于已删除迁移的所有迁移,取决于压缩的迁移。
  • Removing the replaces attribute in the Migration class of the squashed migration (this is how Django tells that it is a squashed migration).

一旦您压制了迁移,您就不应该重新压缩被压缩的迁移,直到您完全转换为正常迁移为止。

序列化值

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

虽然Django可以序列化大多数东西,但有一些,我们不能序列化为有效的Python表示 - 对于如何将值转换回代码没有Python标准(repr()仅适用于基本值,并且不指定导入路径)。

Django可以序列化以下内容:

  • int, long, float, bool, str, unicode, bytes, None
  • dictlisttupleset
  • datetime.datedatetime.datetimedatetime.time实例(包括时区感知的实例)
  • decimal.Decimal个实例
  • enum.Enum实例
  • uuid.UUID个实例
  • functools.partial具有可串行化funcargskeywords值的实例。
  • LazyObject包含可序列化值的实例。
  • 任何Django字段
  • 任何函数或方法引用(例如datetime.datetime.today)(必须在模块的顶级作用域中)
  • 任何类引用(必须在模块的顶级作用域中)
  • 使用自定义deconstruct()方法(see below)的任何内容
在Django更改1.10:

添加了enum.Enum的序列化支持。

在Django更改1.11:

已添加uuid.UUID的序列化支持。

Django只能在Python 3上序列化以下内容:

  • 在类体内使用的未绑定方法(见下文)

Django不能序列化:

  • 嵌套类
  • 任意类实例(例如MyClass(4.3, 5.7)
  • Lambdas

由于__qualname__仅在Python 3中引入,Django只能在Python 3上序列化以下模式(在类体中使用的未绑定方法),但将在Python 2上无法序列化对它的引用:

class MyModel(models.Model):

    def upload_to(self):
        return "something dynamic"

    my_file = models.FileField(upload_to=upload_to)

如果您使用的是Python 2,我们建议您移动upload_to的方法以及接受可调用项(例如default)的类似参数,使其位于主模块正文中,而不是类主体中。

添加一个deconstruct()方法

您可以让Django通过为类提供deconstruct()方法来序列化您自己的自定义类实例。 它不需要参数,应该返回一个三元组的元组(path, args, kwargs)

  • path应该是类的Python路径,类名称作为最后一部分(例如,myapp.custom_things.MyClass)。 如果你的类在模块的顶层不可用,那么它是不可序列化的。
  • args应该是要传递给类'__init__方法的位置参数列表。 这个列表中的所有内容都应该是可序列化的。
  • kwargs应该是传递到类'__init__方法的关键字参数的dict。 每个值本身都是可序列化的。

此返回值不同于自定义字段的deconstruct()方法for custom fields

Django将写出这个值作为你的类的实例化与给定的参数,类似于它写出对Django字段的引用的方式。

为了防止每次运行makemigrations时创建新的迁移,您还应该向装饰类添加一个__eq__()方法。 这个函数将被Django的迁移框架调用来检测状态之间的变化。

只要你的类的构造函数的所有参数都是可序列化的,你可以使用@deconstructible中的deconstruct()类装饰器来添加django.utils.deconstruct方法:

from django.utils.deconstruct import deconstructible

@deconstructible
class MyCustomClass(object):

    def __init__(self, foo=1):
        self.foo = foo
        ...

    def __eq__(self, other):
        return self.foo == other.foo

装饰器添加逻辑来捕获和保存参数到它们的构造函数中,然后在调用deconstruct()时返回这些参数。

支持Python 2和3

为了生成支持Python 2和3的迁移,您的模型和字段中使用的所有字符串文字(例如verbose_namerelated_name),必须始终是Python 2和3中的测试或文本(unicode)字符串(而不是Python 2中的字节和Python 3中的文本,未标记字符串文字的默认情况)。 否则在Python 3下运行makemigrations会生成虚假的新迁移,将所有这些字符串属性转换为文本。

The easiest way to achieve this is to follow the advice in Django’s Python 3 porting guide and make sure that all your modules begin with from __future__ import unicode_literals, so that all unmarked string literals are always unicode, regardless of Python version. 当您将其添加到具有在Python 2中生成的现有迁移的应用程序时,您在Python 3上下一次运行makemigrations可能会产生许多更改,因为它将所有的测试属性转换为文本字符串;这是正常的,应该只发生一次。

支持多个Django版本

如果您是使用模型的第三方应用的维护者,则可能需要提供支持多个Django版本的迁移。 在这种情况下,您应始终使用希望支持的Django版本运行makemigrations

迁移系统将根据与其他Django相同的策略保持向后兼容性,因此在Django X.Y上生成的迁移文件应该在Django X.Y + 1上保持不变。 然而,迁移系统不承诺向前兼容性。 可能会添加新功能,并且使用较新版本的Django生成的迁移文件可能无法在旧版本上运行。

请参见

迁移操作参考
涵盖架构操作API,特殊操作和编写自己的操作。
写作迁移“如何”
介绍如何构建和编写可能遇到的不同方案的数据库迁移。