@Lenciel

Data Migration in Django 1.7 (1)

Django 1.7 已经发布一段时间了,基本上这个版本最主要的改动就是加入了migrations

在过去,几乎所有的 Django 项目都是用 South 来处理数据变更的。而在 Django1.7 版本,South 的作者 Andrew Godwin 把migrations加到了 Django Core 里面。

So…

Migrations是什么?

Migrations 其实就是一堆帮助你完成数据库变更和数据迁移的命令,使得你可以用「Django」的方式来管理和变更数据库的 schema。比如,当你的 model 改变了,你需要在数据库里面去重命名一列时,你不会想跑到命令行下面去敲 SQL 吧?特别是,如果你要变更的数据库是线上的,有几百万用户数据,你应该更不愿意搭上这种活了吧?

Migrations 让事情变得简单可控:

  1. 它使得数据库 schema 的调整可以通过 Django 命令来完成
  2. 它使得数据库的 schema 和对应的 model 的变更被 track 起来:整个历史都可以版本化在 Git 里面
  3. 提供了一套匹配 schema 和对应的 fixture 的机制
  4. 如何和 CI 搭配起来,可以保证代码和数据一致性

Migrations上手

创建测试项目

首先创建一个 virtualenv 和 django 项目:

$ mkvirtualenv django17
$ pip install https://www.djangoproject.com/download/1.7c2/tarball/
$ django-admin.py startproject django_migration_test
$ cd django_migration_test
$ python manage.py startapp ts_data

然后创建一个 model 到 subl ts_data/models.py:

from django.db import models

# Create your models here.
class PingPongPrice(models.Model):
    date = models.DateTimeField(auto_now_add=True)
    price = models.DecimalField(max_digits=5, decimal_places=2)

subl django_migration_test/settings.py

INSTALLED_APPS = (
    ...
    'ts_data',
)

创建Migrations

使用下面的命令可以创建 ts_data 这个 app 的 Migrations。当然,和大多数 Django 命令一样,如果你不显式的指定,就

(django17) ○ python manage.py makemigrations ts_data
Migrations for 'ts_data':
  0001_initial.py:
    - Create model PingPongPrice

应用Migrations

(django17) ○ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, contenttypes, ts_data, auth, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying sessions.0001_initial... OK
  Applying ts_data.0001_initial... OK

注意,因为是一个全新的 app,这条命令会先建表,换句话说,之前版本的syncdb命令可以不用了。整个使用流程应该变成:

  1. 建立或者更新一个 model
  2. 运行python manage.py makemigrations <app_name>
  3. 运行python mange.py migrate <app_name来应用创建的 Migrations
  4. 重复前面的步骤

不是新建的项目如何使用

大多数情况下我们都是从旧版本的 Django 迁移过来,也就意味着是从 South 迁移过来。这种情况下需要:

  1. 删除所有的 South 创建的 migration 文件
  2. 运行 ./manage.py makemigrations,Django 会根据你当前 model 来创建那份initial migrations file
  3. 运行./manage.py migrate,Django 会把已经存在的数据库 table 当成是 migration 的产物,完成整个 migration

如果你运行上面的命令遇到错误,就需要运行 ./manage.py migrate --fake <appname> 做一个 fake 的 migration。

如果你不想丢掉过去的 South 维护的历史记录,可以同时使用 South 和 Django Migrations:升级 South 到 1.0,然后参考这篇文章的做法

South和Django Migrations比较

对比一下 South 和 Django Migrations 的 workflow,可能会更加清晰:

首次全新migrations

South:

./manage.py syncdb
./manage.py schemamigration <appname> --initial

Django Migrations:

./manage.py makemigrations <appname>

应用migrations

South:

./manage.py migrate <appname>

Django Migrations:

./manage.py migrate <appname>

非首次migrations

South:

./manage.py schemamigration <appname> --auto

Django Migrations:

./manage.py makemigration <appname>

可以看到,大概是因为出自同一个作者的原因,Django Migrations 基本上 follow 了 South 的工作流程,只不过是命令更加简洁和清晰了。

更多细节

哪些变化会被Django Migrations找到?

如果你再次运行python manage.py migrate,会发现什么都没有发生:这是因为在项目的数据库中有一张django_migrations仍然被更新。表,记录了哪些 Migrations 已经被应用过了:无论是运行了 migrate 还是 fake 的,这个表都会被插入一条记录。比如从 South 升级到使用 Django 自带的 MigrationsDjango 会检查是否有更新。如果没有,它就 fake 一次,但django_migrations仍然被更新。

在少数情况下,确实有需要再次运行某个特定的 Migrations,我们可以在django_migrations里面把这个记录删除掉。

在极少数情况下,如果你有需要回退到特定的版本,比如最初的 zero 版本,可以用类似python manage.py migrate <app_name> zero的语法。

Migration 文件

在我们运行python manage.py migrate <app_name>究竟发生了什么?实际上,Django 会创建一个 python 文件来描述如何完成这个 migration,以前面的 ts_data 为例,这个文件位于ts_data/migrations/0001_initial.py,内容如下:

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

from django.db import models, migrations


class Migration(migrations.Migration):

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='PingPongPrice',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('date', models.DateTimeField(auto_now_add=True)),
                ('price', models.DecimalField(max_digits=5, decimal_places=2)),
            ],
            options={
            },
            bases=(models.Model,),
        ),
    ]

可以看到,是完全可读的 Python 代码。这也是为什么推荐把整个migrations文件夹加入版本控制的原因:这样你的应用经过了怎样的变更就变得可以回溯了。

Migration Dependencies

上面的源代码有一些值得注意的地方。

首先,所有的 migration file 里面都有一个Migration()类,继承自django.db.migrations.Migration。在我们运行migrate命令的时候,运行的就是这个类。

这个类有两个 list,一个是dependencies,一个是operations

dependencies定义了这个 migration 之前必须完成的操作,比如你的 model 里面包括一个外键,那么你得首先有对应的 table。比如,假设外键指向的 model 在app_1,那么dependencies会像这样:

dependencies = [
   ('main', '__first__'),
]

如果没有前置条件,这个 list 可以为空。但大多数时候dependencies是指向其他的 migration 文件。比如:

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

这里使用 list 的结果是,所有的依赖是没有顺序的,也就是说你不需要按照 0001、0002、0003 的顺序来排列所有的依赖。

Migration Operations

这个 list 定义的就是 migration 完成的操作,可以分为下面的这些种类:

  • CreateModel
  • DeleteModel
  • RenameModel
  • AlterModelTable
  • AlterUniqueTogether
  • AlteIndexTogether
  • AddField
  • RemoveField
  • RenameField
  • RunSQL
  • RunPython

前面的那些操作是整个 Django Migrations 的核心:因为需要对各种不同的数据库做适配。而后面的两个操作则是灵活度非常高的,几乎可以干任何事情。

实例

让我们试试把PingPongPriceprice这个 field 的max_digits改成 8 位的(通货膨胀嘛),然后再次运makemigrations行命令:

(django17) ○ python manage.py makemigrations ts_data
Migrations for 'ts_data':
  0002_auto_20140805_1525.py:
    - Alter field price on PingPongPrice

可以看到这次生成的 migration 文件里面有AlterField操作:

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

from django.db import models, migrations


class Migration(migrations.Migration):

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

    operations = [
        migrations.AlterField(
            model_name='PingPongPrice',
            name='price',
            field=models.DecimalField(max_digits=8, decimal_places=2),
        ),
    ]

欢迎留言