django 1.7 新的migration框架,取代south

仲孙焱
2023-12-01

from:http://www.withfan.com/blog/django-17-%E6%96%B0%E7%9A%84migration%E6%A1%86%E6%9E%B6%E5%8F%96%E4%BB%A3south/

https://docs.djangoproject.com/en/dev/topics/migrations/

迁移

Django 1.7i 新特性

Migrations是Django的方式将模型变更(添加一个字段,删除模型等)映射到数据库schema。它们设计成大部分都是自动进行的,但你需要知道什么时候做迁移,和运行迁移时你可能遇到的常见问题。

一个简短的历史

1.7版本之前,Django只支持新增模型到数据库中;通过syncdb(migrate的前身)命令变更或者删除现存的模型市不可能的。 

第三方工具,尤其是South,支持这些增加的变更类型,但是现在被认为已经足够重要到需要引入到django的core中去。

2条命令

你将使用2条命令进行迁移和操纵数据库schema:

  • migrate,用来使迁移生效,以及未生效时报告它们的状态。
  • makemigrations,用来根据你对模型做的变更创建新的迁移脚本。

值得注意的是,迁移是创建和运行在每个应用程序的基础上。特别是,对部分应用程序不使用迁移是可能的(称为“不迁移”的应用),这些应用程序将取代模拟只是添加新的模型传统的行为。

你应该把迁移作为你的数据库架构的版本控制系统。makemigrations负责包装你的模型变更到独立的迁移文件中-类似于提交代码-migrate负责应用到你的数据库中。

迁移文件存放在每个应用程序的一个“migrations”目录中,被设计成提交,和作为发布的一部分,到代码库中。你应该在你的开发机器上使用它们,然后在你的同事的机器上运行相同的迁移脚本,staging machines,最终在你的生产机器。

注意

在逐个app基础上,覆盖存放迁移脚本的包的名字是可以的,只要修改MIGRATION_MODULES设置。

迁徙将在相同的数据集上以相同的方法运行并产生一致的结果,这意味着你将看到在开发阶段,staging阶段,在相同的情况下,和生产环境上的表现完全一致。

Django会迁移你对模型或字段所做的任何变更 - 甚至是不影响数据库的选项 - 因为唯一能正确的重建一个字段的方式是记录它发生所有的变更,而你可能需要这些选项应用在以后的一些数据迁移中(例如,如果你设置了自定义验证器)。

后端支持

迁移支持django使用的所有后端,以及所有支持schema变更(通过SchemaEditor类完成)的第三方后端。

然后,一些数据库有更好的schema迁移支持能力;在下面描述了一些注意事项。

PostgreSQL

PostgreSQL在所有数据库中有最好的schema支持能力;唯一的注意事项是添加一个有默认值的列时会导致表全部重写,所需时间和表的规模成比例。

因为这个原因,推荐你创建新列时总是设置成null=True,这样能立即添加。

MySQL

MySQ对于schema变更操作缺乏事务支持,意味着如果迁移失败,你必须手动回退变更然后才能重新迁移。 (回滚到之前的某个点是可能的)。

另外,MySQL对几乎全部schema操作都会完全重写表,增加或删除列会花费一定时间,和行的数量成比例。在低速硬件上每百万行数据可能要花一分钟以上 -给几百万行数据的表添加几列能锁定你的站点10分钟以上。

最后, MySQL对列名,表名和索引名有合理的长度限制,以及单一索引能覆盖的所有列的组合大小也有限制。这意味着在其他后端上可能创建的索引,在MySQL上可能失败。

SQLite

SQLite只有非常少的内置schema变更支持, 因此Django尝试通过以下方法进行模拟:

  • 使用新的schema创建一个新表
  • 表间数据复制
  • 删除旧表
  • 新表重命名为旧表

这个过程通常工作的很好,但有可能比较慢并且偶尔会出现bug。不推荐你在生产环境上运行和迁移SQLite,除非你非常清楚它的风险和限制; Django使用SQLite是为了让开发者在他们的本机上开发不是那么复杂的项目,因此不需要一个完全的数据库。

工作流

使用迁移是非常简单的。变更你的模型 - 比如,添加一列或删除一个模型 - 然后运行makemigrations:

$ python manage.py makemigrations
Migrations for 'books':
  0003_auto.py:
    - Alter field author on book

你的模型会被扫描并和当前版本迁移文件中的模型进行比较,然后生成一系列新的迁移文件。确保阅读输出来看看 makemigrations做的是不是你想要的 - 这种方式并不完美,对于复杂的变更它也不能如你期望的那样检测出来。

一旦你有了这些新的迁移文件,你就可以将它们应用到数据库并确保它们如期望般工作:

$ python manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: sessions, admin, messages, auth, staticfiles, contenttypes
  Apply all migrations: books
Synchronizing apps without migrations:
  Creating tables...
  Installing custom SQL...
  Installing indexes...
Installed 0 object(s) from 0 fixture(s)
Running migrations:
  Applying books.0003_auto... OK

命令分2步运行;第一步,它同步不迁移的app (和之前版本提供的syncdb功能一样),然后开始运行所有没有应用的迁移。

一旦运行了迁移,将迁移和模型变更在同一次提交中提交到你的版本控制系统 - 这样,当其他开发者 when other developers (或者你的生产服务器) 检出代码时,他们能同时获得变更的模型和迁移脚本。

版本控制

因为迁移脚本存放在版本控制系统中,偶尔你会碰到这样的情况,当你和另一个开发者同时提交了同一个app的迁移脚本,导致2个迁移脚本有同样的顺序号。

不用担心 - 顺序号只是为了开发者引用,Django只关心不同名称的迁移脚本。迁移在脚本中指定了它依赖的其他迁移脚本- 包括在同一个app上的之前的迁移,所以是可能检测到这2个对同一个app的新的迁移不是有先后顺序的(即有冲突的)。

当这种情况发生时,Django会提示你并给你一些选项。如果你认为这足够安全, 它能自动的线性化的应用这2个迁移。 如果不是,你需要自己去修改迁移脚本- 别担心,这不难,下面的Migration files也会详细解释。

依赖

迁移是逐app进行的,模型的表和关系可能过于复杂而不能在单一app上创建。当进行迁移时可能会要求其他东西也要运行 - 例如,你在book app中添加一个外键指向authors app - 生成的迁移脚本就会包含一个authors中的迁移脚本的依赖。

这意味着当你运行迁移时, authors迁移先运行来创建这个ForeignKey引用的表,然后生成ForeignKey的列的迁移运行并创建这个约束。如果不是这样,迁移会尝试创建一个ForeignKey列指向一个还不存在的表,这时你的数据库会抛出异常。

这种依赖行为影响的大多数迁移操作被限制到一个单一的应用程序。限制到一个单一的应用程序(无论是makemigrations或migrate)是一个最好的努力的承诺,但不是保证;任何需要使用其他应用程序需要得到的正确的依赖关系。

但是要注意,不迁移的应用程序不能依赖于应用程序的迁移,不迁移没有依赖是很自然的。这意味着,一个不迁移的应用程序不能有一个外键或多对多的关系指向一个迁移应用程序;某些情况下可以工作,但是它最终会失败。

如果你使用可替换的模型(例如AUTH_USER_MODEL)时是很明显的,因为每个使用可替换模型的app都需要进行迁移,如果你不走运的话。随着时间推移,越来越多的第三方应用需要迁移,但同时你也可以自己进行迁移(使用MIGRATION_MODULES存放app之外的模型),或者保持你的用户模型不进行迁移。

迁移脚本

迁移文件被存放为一种磁盘格式,这里称之为“迁移脚本”。这些文件实际只是普通的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子类。它检测这个对象的4种属性,只有2个在大多数时候使用:

  • 依赖,一个迁移依赖的列表。
  • 操作,一个定义了迁移如何进行的Operation类的列表。

操作是关键,他们是一系列指示告诉django怎么进行schema更新。django扫描它们并在内存中构建所有app的schema更新,用来生成对应的SQL语句。

这些内存中的结构也用来找出模型与当前迁移状态的差异点。django运行所有的变更,顺序的,通过刷新内存中的模型到你最后一次运行makemigrations时的模型状态。它通过使用这些模型与你的models.py文件做对比找出你所做的变更。

你很少需要手动编辑迁移脚本,但是需要的话也是完全有可能手写的。一些更复杂的操作不能自动检测到,只能通过手写,所以当需要编辑的时候也不要被吓到。

自定义字段

你不能修改已经迁移过的自定义字段的位置参数的数量,会导致TypeError异常。老的迁移会用老的签名调用修改的__init__方法。所以如果你需要一个新的参数,请创建一个keyword argument并且在构造函数中加上assert kwargs.get('argument_name') is not None之类的语句。

增加迁移到应用中

增加迁移到新app中是很直接的 - 它们预置为接受迁移,所以当做出变化后只需要运行makemigrations

如果你的app已经有模型和数据库表,而且没有使用过迁移 (例如,你在之前的django版本上创建的),你需要转换到使用迁移模式;下面是一个简单的过程:

$ python manage.py makemigrations your_app_label

它将会为你的app初始化迁移。现在,当你运行migrate,django能检测到你已经初始化迁移,而那些它想创建的表已经存在,它就会标记迁移已经应用。

注意这能正常工作需要2个条件:

  • 生成表之后没有变更过模型。为了迁移能工作,需要先做初始化迁移工作,然后再变更模型,因为django和迁移脚本做变更对比,而不是数据库。
  • 没有手动修改过数据库 - django不能检测到你的数据库和模型的不匹配,当使用迁移修改这些表时只能得到error。

模型演变

当你运行迁移, django通过存储在迁移脚本中的模型历史版本来运行。如果你使用RunPython 操作运行Python代码,或者你在数据库routers上添加了allow_migrate 方法, 你的模型将被暴露给这些版本。

因为不能序列化任意的Python代码,这些历史版本模型不会包含自定义方法和manager。它们,包含同样的字段,关系和元数据 (也版本化了,所以可能和你当前的不一致)。

警告

这意味着当你在迁移时不能访问模型对象上的自定义save()方法,也不能有任何自定义的构造或实例化方法,请仔细规划。

另外,模型的基类仅仅被存储为指针,所以如果迁移中包含对它们的引用你需要一直保留这些基类。好的方面而言,基类中的方法和managers能正常的继承,所以如果你确实需要访问它们你可以选择把它们移到基类中。

数据迁移

当变更数据库schema时,你也可以结合schema使用migrations变更数据库中的数据。

变更数据的迁移被称为“data migrations”;它们最好写成单独的脚本,放在你的架构迁移脚本旁边。

django不会自动为你创建数据迁移脚本,它只会生成架构迁移,但是写这些数据迁移也不是很难。django中的迁移脚本由 Operations组成,数据迁移的主要操作是RunPython

开始时,生成一个空的迁移脚本(django会放置文件在正确的地方,建议一个文件名称,并添加依赖):

python manage.py makemigrations --empty yourappname

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

# -*- coding: utf-8 -*-
from django.db import models, migrations

class Migration(migrations.Migration):

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

    operations = [
    ]

现在,你所需要做的就是创建一个新的函数让RunPython调用。RunPython期望一个callable对象,有2个参数 - 第一个是一个app registry,它含有你的模型的全部历史变更信息,第二个是SchemaEditor,用来手动变更数据库架构(注意,这个可能和迁移自动检测想冲突!)

让我们写一个简单的脚本生成一个新字段name,它是first_name和last_name的组合值 (我们需要意识到不是所有人都有 first 和 last name)。我们需要做的是使用历史模型,迭代所有的行:

# -*- coding: utf-8 -*-
from django.db import models, 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,数据迁移会和其他迁移一同完成。

你也可以传递第二个callable 给 RunPython 来运行任何你想要的逻辑。如果这个callable被省略了,迁移后端会抛出异常。

如果你希望阅读更多的高级迁移操作介绍,或者考虑能否自己写,参考 migration operations reference

合并迁移

你可以自由使用migrations不用考虑迁移数量。migration代码被优化成可以一次处理几百个迁移而不会明显变慢。然而,偶尔你需要将几百次迁移变成少量迁移,这时你需要使用squashing。

Squashing是减少现存的大量迁移到一个(有时是几个)迁移的艺术,代表了同样的变更。

Django做到这个是通过收集所有现存的迁移脚本,抽起它们的操作,然后把它们放到一个序列中,对它们进行一个优化来减少序列的长度 - 例如,它知道CreateModel 和 DeleteModel 操作相互抵消,它也知道AddField可以整合到CreateModel中。

一旦操作序列被尽可能的减少 - 这个数量可能取决于你的模型相互缠结的程度和是否有RunSQL 或 RunPython 操作 (这些都不能被优化) - Django将把它们写回到一套新的初始化迁移脚本中。

这些脚本被标记为取代之前的脚本 - 合并脚本,所以它们能够和旧的脚本共存,Django能智能的在它们之中切换。如果你还处于合并迁移过程中,它会继续使用原来的脚本直到合并脚本的最后一项,然后切换到合并历史版本,这样新安装时会只使用新的合并脚本而跳过所有老的脚本。

这保证了你能进行合并而不会弄乱并非完全保持最新的生产系统。推荐流程是进行合并,保留老的脚本,提交和发布,等待所有系统都升级到新的版本(或者如果是第三方工程,保证你的用户顺序升级而不要跳过任何一步),然后删除老的文件,提交和进行下个版本开发。

命令是squashmigrations - 只需要传递app的label 和 你想合并的迁移脚本名称,它就可以工作了:

$ ./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,不过请报告一个bug( file a bug report),告知模型细节和它们的关系,这样我们可以针对它改善优化器。

一旦你合并了你的迁移,你应该和原迁移一起提交并发布变更到所有的你的运行中的应用上,确保它们运行migrate并将变更保存到数据库中。

完成之后,你需要将合并迁移脚本转换成一个普通的初始化迁移脚本,通过:

  • 删除所有被代替的迁移脚本
  • 在合并迁移脚本中的Migration类中删除replaces参数 (这是Django如何获知他是合并脚本的方式)

注意

一旦你合并了迁移,你不能重复合并直到你把它完全转换成一个普通的脚本。

序列化值

迁移脚本仅仅是包含了老的模型定义的Python文件 - 因此, django需要获取模型的当前状态并把它们序列化输出到一个文件。

django能序列化大多数,但还是有部分我们不能序列化成一个合法的Python表示 - 没有相关的Python标准来说明一个值应该怎么转换成代码 (repr()只能识别基本类型,也不能指定import路径)。

django能序列化以下值:

  • intlongfloatboolstrunicodebytesNone
  • listsettupledict
  • datetime.date 和 datetime.datetime 实例
  • decimal.Decimal 实例
  • 任意Django域
  • 任意function或method引用(例如 datetime.datetime.today)
  • 任意class引用
  • 任意有自定义deconstruct() 方法的对象 (see below)

django 在 Python 3 能序列以下:

  • 类body中使用的未绑定 methods  (see below)

django不能序列以下类型:

  • 任意 class 实例 (例如 MyClass(4.3, 5.7))
  • Lambdas

因为 __qualname__ 只在Python 3中引入,django只能在Python 3中序列化以下pattern (类body中使用的未绑定 methods), 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 或者相似的接受callables参数的方法 (例如default)移到主module体中,而不是类体中。

增加一个deconstruct()方法

你可以让django来序列化你的自定义类实例,只有给类一个deconstruct()方法。 方法不需要参数,需要返回一个包括3个东西的元组: (path, args, kwargs)。注意返回值不同与自定义域的deconstruct()方法,这个方法返回包含4项的元组。

path 指向类的Python路径,最后部分包含类名(例如myapp.custom_things.MyClass)。如果你的类通过顶层模块无法访问,那么它不会被序列化。

args 传递给类的__init__方法的位置参数的列表。列表中的参数本身也必须是支持序列化的。

kwargs 传递给类的 __init__方法的关键字参数的字典。字典中的每个值都必须是支持序列化的。

django会为你的类的实例化写入值,包括给定的参数,和写django字段引用的方法类似。

因为你的类的constructor的参数本身会序列化,你可以从django.utils.deconstruct导入@deconstructible类装饰器:

from django.utils.deconstruct import deconstructible

@deconstructible
class MyCustomClass(object):

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

这个装饰器加入了逻辑能捕获和保存进入构造器的参数,然后当deconstruct()调用时精确的返回这些参数。

从South升级

如果你已经使用South做过迁移,那么升级到使用django.db.migrations是非常简单的:

  • 确保所有的安装应用和它们的迁移状态一致
  • 删除所有的编号迁移脚本,但不用删除目录或__init__.py - 同时确保你删除了.pyc文件。
  • 运行python manage.py makemigrations。Django会发现这个空的迁移文件夹并且用新的格式生成新的初始化迁移脚本。
  • 运行python manage.py migrate。Django会发现初始化迁移要创建的表已经存在,它会标记为已应用这些迁移。

完成了! 唯一不好的是如果你有一个外键循环引用,这种情况下, makemigrations会不止一次执行初始化迁移,所以你需要像下面这样标记它为已经应用:

python manage.py migrate --fake yourappnamehere

库/第三方应用

如果你是库或app维护员,希望能同时支持South (为Django 1.6和以下版本) 和 Django migrations (为1.7以上版本),你需要保留2套并行的迁移设置,每种一种格式。

为了达到这个目的,South 1.0会自动寻找South-format migrations,首先在south_migrations目录中,在使用migrations之前,意味着用户的项目会透明的使用正确的这套设置只要你把South migrations放在south_migrations目录中,把Django migrations放在migrations目录中。

 类似资料: