Django 1.7 自帶migrations用法及源碼

Django下一個版本是1.7,增長了相似South的migration功能,修改Model後能夠在不影響現有數據的前提下重建表結構。這真是個千呼萬喚始出來的feature了,因此作個簡單的整理分享。文章包含部分源代碼,對具體怎麼實現不感興趣能夠忽略。 python

Prepare

從Django官網或直接pip下載1.7b版本,建立project和app: mysql

$ 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,如下是對這兩個文件的修改: git

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文件的操做)命令建立數據庫。 github

makemigrations

首先要建立migrations,新版Django執行manager.py startapp會生成migrations/目錄,makemigrations命令生成的文件會存到migrations/目錄下。 sql

$ 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文件使用的是如下代碼: 數據庫

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代碼寫到文件中。 django

MigrationWriter(Line 1)在writer.py文件中定義。Python代碼用縮進來劃分邏輯,下面這段代碼用了三個方法(indent/unindent/feed),調用indent/unindent時給self.indentation增/減1,須要縮進時調用feed方法補上對應的空格實現縮進: session

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: app

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 )
$ 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,不然視爲建立。 post

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文件開始的:

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,開發就更美好了:)

相關文章
相關標籤/搜索