首页 > 代码库 > Django 1.7 自带migrations用法及源码
Django 1.7 自带migrations用法及源码
Django下一个版本是1.7,增加了类似South的migration功能,修改Model后可以在不影响现有数据的前提下重建表结构。这真是个千呼万唤始出来的feature了,所以做个简单的整理分享。文章包含部分源代码,对具体怎么实现不感兴趣可以忽略。
Prepare
从Django官网或直接pip下载1.7b版本,创建project和app:
$ python manage.py startproject dmyz
$ cd dmyz/
$ python manage.py startapp articles
修改articles/modules.py文件,增加Article,accounts到dmyz/settings.py文件的INSTALLED_APPS,以下是对这两个文件的修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# ===== 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/目录下。
Migrations for ‘articles’:
0001_initial.py:
- Create model Article
$ ls articles/migrations/
__init__.py 0001_initial.py
创建migrations/文件夹,写入__init__.py文件和migration文件使用的是以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 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方法补上对应的空格实现缩进:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
# 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:
1
2
3
4
|
# articles/modules.py
class
Article
(
models
.
Model
)
:
title
=
models
.
CharField
(
max_length
=
18
,
null
=
True
)
author
=
models
.
OneToOneField
(
User
,
null
=
True
)
|
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命令:
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文件开始的:
1
2
3
4
5
6
|
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的方法,真正实现对数据库的操作,主要代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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,开发就更美好了:)
Django 1.7 自带migrations用法及源码