Python Tech

Django 1.7 自带migrations用法及源码

Django下一个版本是1.7,增加了类似South的migration功能,修改Model后可以在不影响现有数据的前提下重建表结构。这真是个千呼万唤始出来的feature了,所以做个简单的整理分享。文章包含部分源代码,对具体怎么实现不感兴趣可以忽略。

Prepare

从Django官网或直接pip下载1.7b版本,创建project和app:

$ pip install https://www.djangoproject.com/download/1.7b2/tarball/
$ python manage.py startproject dmyz
$ cd dmyz/
$ python manage.py startapp articles

修改articles/modules.py文件,增加Article,accounts到dmyz/settings.py文件的INSTALLED_APPS,以下是对这两个文件的修改:

# ===== articles/modules.py =====
# encoding: utf8
from django.db import models
from django.contrib.auth.models import User

class Article(models.Model):
    title = models.CharField(max_length=18, null=True)

# ===== dmyz/settings.py =====
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'articles',
)

在dmyz/settings.py文件中调整数据库设置。按照官方文档的说明,支持得最好的是postgresql数据库,其次是mysql,目前sqlite不能实现完整的migration功能。本文是在64位Window+Cgywin环境下写的,使用的是mysql5.6版。设置完成后执行syncdb(不执行syncdb也不影响执行makemigrations创建migration文件的操作)命令创建数据库。

makemigrations

首先要创建migrations,新版Django执行manager.py startapp会生成migrations/目录,makemigrations命令生成的文件会存到migrations/目录下。

$ python manage.py makemigrations articles
Migrations for ‘articles’:
  0001_initial.py:
    – Create model Article
$ ls articles/migrations/
__init__.py 0001_initial.py

创建migrations/文件夹,写入__init__.py文件和migration文件使用的是以下代码:

# django/core/management/commands/makemigrations.py
writer = MigrationWriter(migration)
if self.verbosity >= 1:
    self.stdout.write("  %s:\n" % (self.style.MIGRATE_LABEL(writer.filename),))
    for operation in migration.operations:
        self.stdout.write("    - %s\n" % operation.describe())
# 如果增加 --dry-run参数就不写入migration文件,只显示描述结果
if not self.dry_run:
    migrations_directory = os.path.dirname(writer.path)
    if not directory_created.get(app_label, False):
        if not os.path.isdir(migrations_directory):
            os.mkdir(migrations_directory)
        init_path = os.path.join(migrations_directory, "__init__.py")
        if not os.path.isfile(init_path):
            open(init_path, "w").close()
        # We just do this once per app
        directory_created[app_label] = True
    migration_string = writer.as_string()
    with open(writer.path, "wb") as fh:
        fh.write(migration_string)

检测app目录下是否存在migrations/目录,不存在就新建,再以write的方式操作__init__.py文件,最后把生成的migration代码写到文件中。

MigrationWriter(Line 1)在writer.py文件中定义。Python代码用缩进来划分逻辑,下面这段代码用了三个方法(indent/unindent/feed),调用indent/unindent时给self.indentation增/减1,需要缩进时调用feed方法补上对应的空格实现缩进:

# django/db/migrations/writer.py
imports = set()
for arg_name in argspec.args[1:]:
    arg_value = normalized_kwargs[arg_name]
    if (arg_name in self.operation.serialization_expand_args and
            isinstance(arg_value, (list, tuple, dict))):
        if isinstance(arg_value, dict):
            self.feed('%s={' % arg_name)
            self.indent()
            for key, value in arg_value.items():
                arg_string, arg_imports = MigrationWriter.serialize(value)
                self.feed('%s: %s,' % (repr(key), arg_string))
                imports.update(arg_imports)
            self.unindent()
            self.feed('},')
        else:
            self.feed('%s=[' % arg_name)
            self.indent()
            for item in arg_value:
                arg_string, arg_imports = MigrationWriter.serialize(item)
                self.feed('%s,' % arg_string)
                imports.update(arg_imports)
            self.unindent()
            self.feed('],')
    else:
        arg_string, arg_imports = MigrationWriter.serialize(arg_value)
        self.feed('%s=%s,' % (arg_name, arg_string))
        imports.update(arg_imports)
self.unindent()
self.feed('),')
return self.render(), imports

def indent(self):
    self.indentation += 1

def unindent(self):
    self.indentation -= 1

def feed(self, line):
    self.buff.append(' ' * (self.indentation * 4) + line)

def render(self):
    return '\n'.join(self.buff)

接下来修改articles/models.py,增加一个field,再次执行makemigrations:

# articles/modules.py
class Article(models.Model):
    title = models.CharField(max_length=18, null=True)
    author = models.OneToOneField(User, null=True)
$ python manage.py makemigrations articles
Migrations for ‘articles’:
  0002_article_author.py:
    – Add field author to article

自动检测新旧两个modle的差异是一个相当麻烦的工作,autodatector.py的代码比其他文件都长,但逻辑是很清晰的。主要是从上一个migration中获取之前的Model列表,写到set中,现有Model也是同样的操作,遍历这两个set的差集,获取差集Model中所有的field,如果field的定义相同,就询问用户是否是一个rename的model,否则视为创建。

autodatectory.py在测试的过程中raise了几个错误,代码量也不少,所以只附上源代码链接,不贴在原文里了:
https://raw.githubusercontent.com/django/django/stable/1.7.x/django/db/migrations/autodetector.py

migrate

之前的两次makemigrations操作只是生成migration文件,还没有对数据库进行操作,接下来执行migrate命令:

$ python manage.py migrate articles
Operations to perform:
  Apply all migrations: articles
Running migrations:
  Applying articles.0001_initial FAKED
  Applying articles.0002_article_author OK

执行后数据库articles_article这张表会增加author_id字段,执行过的migration文件会记录到django_migrations表中,避免重复执行。带–fake参数执行migrate命令时,只将migration文件记录到数据库的django_migrations表,如果是用South的migration文件,fake操作就很关键了。

这是migration操作中处理数据库的部分,主要代码都在django/db/migrations/operations/目录下,拆分成4个文件:base.py fields.py models.py special.py,和文件名表达的含义一样,文件中是针对Model/Field做Create/Rename/Delete的操作,调用这些文件是从djangp/db/migrations/migration.py文件开始的:

for operation in self.operations:
    new_state = project_state.clone()
    operation.state_forwards(self.app_label, new_state)
    operation.database_forwards(self.app_label, schema_editor, project_state, new_state)
    project_state = new_state
return project_state

在Line 4调用了database_forwards方法,传入的第一个参数是app名称,最后两个参数原state和新的state,里面包含所有字段的定义。schema_editor是根据数据库指定的DatabaseSchemaEditor类,

之后的操作就是各种调用了:调用opration的方法,oprations调用根据具体的操作(add/alter/remove)调用db/backends/数据库类型/schema.py的方法,真正实现对数据库的操作,主要代码如下:

def database_forwards(self, app_label, schema_editor, from_state, to_state): 
    old_apps = from_state.render()
    new_apps = to_state.render()
    old_model = old_apps.get_model(app_label, self.old_name)
    new_model = new_apps.get_model(app_label, self.new_name)
    if router.allow_migrate(schema_editor.connection.alias, new_model):
        schema_editor.alter_db_table( 
            new_model,
            old_model._meta.db_table,
            new_model._meta.db_table,
        )

def alter_db_table(self, model, old_db_table, new_db_table):
    self.execute(self.sql_rename_table % {
        "old_table": self.quote_name(old_db_table),
        "new_table": self.quote_name(new_db_table),
    })

Afterword

这篇文章在草稿箱里存了半年(2013年11月)了,因为花了不少的时间看源码,以及等bug修复,现在的beta2版本修复了之前M2M字段的问题,但autodetector仍然有bug(已经提交到Trac)。

South常年居于最受欢迎的Django应用列表,说明依据Model修改关系数据库结构是开发中的一个重要的问题,解决这个问题可以提升开发速度。当然也只是[开发]速度,关系数据库经常用来存储Web业务的核心数据,是Web应用最常见的性能瓶颈,South这种用了好几年的模块也不敢在生产环境数据库上随便操作,更不用说现在还有Bug的自带migration了。

什么时候关系数据库也能完美的实现freeschema,开发就更美好了:)

avatar

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  Subscribe  
提醒