高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

從Model到Template

Model我們會建了,資料也可以從後台或Shell來做新增/修改/刪除/更新,網址會設定了,Template也大概知道怎麼回事了,其實整個Django的MTV(Model-Template-View)架構到這裡已經介紹得差不多了,接下來我們就是要把資料給串起來。

回來看一下先來看一下我們的book/views.py

1
2
3
4
5
6
7
8
from django.http import HttpResponse
from django.shortcuts import render_to_response

def index(request):
  return render_to_response('book/index.html')

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

接著,我們要來把資料給撈出來,並且把它餵給Template(我知道「撈」、「餵」這些詞有點台,不過我想大家比較聽得懂):

1
2
3
4
5
6
7
8
9
10
from django.http import HttpResponse
from django.shortcuts import render_to_response
from book.models import Book

def index(request):
  books = Book.objects.all()
  return render_to_response('book/index.html', {'books' : books})

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

我們在第6行把資料庫透過Book類別給取出來,然後在第7行再以dictionary型式傳給render_to_response,這樣待會我們在Template裡就可以取用得到books這個變數。

切換到Template templates/book/index.html

Book Index Template (book_index.html) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Book</title>
</head>
<body>
目前總共有{{ books.count }}本書
  <ul>
  {% for book in books %}
    <li>書名:{{ book.title }}({{ book.page }}頁,售價:{{ book.price }})</li>
  {% endfor %}
  </ul>
</body>
</html>

打開瀏覽器,網址輸入:http://127.0.0.1:8000/,應該可以看到類似的畫面:

image

如果沒有看到列表的話,就先到佛心的Admin模組去新增個兩本書,再回來看一下結果。

那書的內容呢?

剛剛只有列表,接下來要把連結跟內容頁給做起來:

templates/book/index.html,在書名上加上連結:

Book Index Template (book_index_2.html) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Book</title>
</head>
<body>
目前總共有{{ books.count }}本書
  <ul>
  {% for book in books %}
    <li>書名:<a href="/books/{{ book.id }}">{{ book.title }}</a>({{ book.page }}頁,售價:{{ book.price }})</li>
  {% endfor %}
  </ul>
</body>
</html>

再來做一下內容templates/book/detail.html

Book Index Template (book_detail.html) download
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
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Book</title>
</head>
<body>

<h1>書名:{{ book.title }}</h1>
<ul>
  <li>頁數:{{ book.page }}頁</li>
  <li>售價:{{ book.price }}元</li>
  <li>出版日期:{{ book.publish_date }}</li>
</ul>

<h1>作者:{{ book.author.name }}</h1>
聯絡資訊:
<ul>
  <li>電話:{{ book.author.tel }}</li>
  <li>Email:{{ book.author.email }}</li>
  <li>地址:{{ book.author.address }}</li>
</ul>

</body>
</html>

同時,book/views.py也要做些調整:

1
2
3
4
5
6
7
8
9
10
11
from django.http import HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from book.models import Book

def index(request):
  books = Book.objects.all()
  return render_to_response('book/index.html', {'books' : books})

def detail(request, book_id):
  book =  get_object_or_404(Book, id = book_id)
  return render_to_response('book/detail.html', {'book' : book})

這邊我又偷懶的用到了short_cuts模組的get_object_or_404,意思就是說如果資料庫裡面找不到這筆資料的話,就會直接丟出HTTP 404的畫面。

如果程式運作正常,你應該可以看到像這樣的畫面:

image

僅為展示效果,所以頁面很陽春,我相信你拿到設計師的HTML頁面應該都比這個漂亮許多倍。

Filters

到目前為止,程式看起來已經會動了!! 但長官可能看到畫面後,可能會神來一筆的說:「那個錢的單位,可以加上千位的逗點符號嗎?」,或是「那個出版日期..可以把年份放最前面,而且用斜線分開嗎?」。

你的第一個想法,可能會是資料撈出來之後,先每一筆都處理過之後再丟給Template。但事實上這種"資料呈現"的工作,應該只要交給Template的Filter來做就好。Template的Filter有點像在Linux系統上的pipe,就是可以透過|符號,把前面的資料丟給下一個執行,直到最後結束為止,來看一些範例:

Template Build-in Filter (filter_demo.html) download
1
2
3
4
5
6
7
8
9
10
{{ "eddie"|upper }}                         # EDDIE
{{ "eddie kao"|title }}                     # Eddie Kao
{{ "this is a book"|cut:" "}}               # thisisabook
{{ ""|default:"未填寫" }}                    # 未填寫
{{ "<b>hello</b>"|striptags }}              # hello
{{ "too long, don't read"|truncatewords:2}} # too long, ...
{{ "hello python"|wordcount }}              # 2

book{{ 1|pluralize }}                       # book
book{{ 10|pluralize }}                      # books

要注意的是,filter在用的時候,|的左右不可以有空白,不然會直接出現TemplateSyntaxError。其它更多的Filter,請參考:https://docs.djangoproject.com/en/1.3/ref/templates/builtins/

看完之後,認份的來完成剛剛老闆交待的修改吧!

出版日期的年份放前面,年、月、日各別用斜線分開

修改年份表示方式 (book_detail_2.html) download
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
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Book</title>
</head>
<body>
<a href="/">回列表頁</a>
<h1>書名:{{ book.title }}</h1>
<ul>
  <li>頁數:{{ book.page }}頁</li>
  <li>售價:{{ book.price }}元</li>
  <li>出版日期:{{ book.publish_date|date:"Y/m/d" }}</li>
</ul>
<h1>作者:{{ book.author.name }}</h1>
聯絡資訊:
<ul>
  <li>電話:{{ book.author.tel }}</li>
  <li>Email:{{ book.author.email }}</li>
  <li>地址:{{ book.author.address }}</li>
</ul>

</body>
</html>

只改了一行就搞定了!

數字加上千位逗點符號

Django也有幫我們寫好這個filter,但因為這個模組預設並沒有安裝/載入,所以在使用上會稍微麻煩一些:

settings.pyINSTALLED_APPS要加上django.contrib.humanize模組:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    'django.contrib.humanize',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'author',
    'book',
)

在使用之前需要用{% load humanize %}載入,然後就可以用了:

修改年份表示方式 (book_detail_3.html) download
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
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Book</title>
</head>
<body>
{% load humanize %}
<a href="/">回列表頁</a>
<h1>書名:{{ book.title }}</h1>
<ul>
  <li>頁數:{{ book.page }}頁</li>
  <li>售價:{{ book.price|intcomma }}元</li>
  <li>出版日期:{{ book.publish_date|date:"Y/m/d" }}</li>
</ul>
<h1>作者:{{ book.author.name }}</h1>
聯絡資訊:
<ul>
  <li>電話:{{ book.author.tel }}</li>
  <li>Email:{{ book.author.email }}</li>
  <li>地址:{{ book.author.address }}</li>
</ul>

</body>
</html>

沒問題的話,應該是這個樣子:

image

打完收工,下班去了!

寫自己的Filter

有時候就是沒有現成的filter可以用(其實是不常發生..),例如老闆又說「那個日期的年份,可以改成民國年份表示嗎?」。大概猜也知道老外不太可能會幫我們寫這種filter,當然要硬改也不是不行,但就失去了我們用這個framework的原意了。我們就來寫個可以產生民國年份的filter吧(其實不過就是把年份減掉1911而已..)

filter本身其實也就是Python的程式碼而已,所以第一個問題應該會是:「filter要寫在哪裡?」

filter有固定的寫法跟位置,你可以把它寫在適合的App底下,它必須放在某個App的templatetags資料夾裡,而且因為它是一個標準的Python模組,所以在templatetags資料夾裡,它必須也要放一個__init__.py。在我們目前這個範例,我把它放在book這個App裡,模組的檔名我給它取做chinese_date.py,整個看起來的資料夾結構大概會長得像這樣:

image

再來,為了讓Django認得你寫的filter,你會需要有一個叫做registertemplate.Library實體變數,所以它的固定寫法會是長得像這樣:

1
2
3
4
5
6
7
8
# encoding: utf-8
from django import template
register = template.Library()

def chinese_date(value):
  return "民國%i%i%i日" % (value.year - 1911, value.month, value.day)

register.filter("chinese_date", chinese_date)

第1行加上encoding: utf-8是因為我們在程式裡面有用到中文字元,如果沒加這行的話會出現SyntaxError。第2、3行就是Django規定filter的固定寫的filter method,最後第8行再把它註冊起來,讓Django看得到它。第8行的註冊方式,也可以用method decorator的寫法:

1
2
3
4
5
6
7
# encoding: utf-8
from django import template
register = template.Library()

@register.filter
def chinese_date(value):
  return "民國%i%i%i日" % (value.year - 1911, value.month, value.day)

效果一樣,不過我個人比較偏好這種寫法。filter寫好了,最後回來原來的Template:

Custom Filter (book_detail_4.html) download
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
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Book</title>
</head>
<body>
{% load humanize %}
{% load chinese_date %}
<a href="/">回列表頁</a>
<h1>書名:{{ book.title }}</h1>
<ul>
  <li>頁數:{{ book.page }}頁</li>
  <li>售價:{{ book.price|intcomma }}元</li>
  <li>出版日期:{{ book.publish_date|chinese_date }}</li>
</ul>
<h1>作者:{{ book.author.name }}</h1>
聯絡資訊:
<ul>
  <li>電話:{{ book.author.tel }}</li>
  <li>Email:{{ book.author.email }}</li>
  <li>地址:{{ book.author.address }}</li>
</ul>

</body>
</html>

{% load chinese_date %}載入剛剛寫好的模組,修改一下filter,最後得樣子應該會長這樣:

image

搞定,下班啦!

更多相關細節,請參考 https://docs.djangoproject.com/en/1.3/howto/custom-template-tags/

還有嗎?

到這裡,其實我們已經將Django的MTV架構跟流程介紹的差不多了,接下來請趁你還有記憶的時候,把官網的文件全部讀一遍吧!

Template

上一章我們學到了怎麼設定網址跟對應的action,但畫面的呈現不可能永遠只有一個Hello BookStore!這麼美好。很多時候程式設計師是需要跟美術設計師合作的,通常的流程是工程師會從設計手上拿到HTML + CSS的檔案,如果你曾經用PHP開發過網站,我猜大概你可能是把拿到的.html改成.php,然後開始HTML裡面塞PHP程式碼。Django的MTV架構也差不多,而且Django雖然是Python寫的,但不同的是你不可以在Template裡寫Python的程式碼,你在Template只能用Template提供的API,以及一些filter跟tag(不管是內建或是自己寫的)。

主要的目的就是希望大家不要把以前那種寫義大利程式碼(Spaghetti code)的不良習慣帶來Django。

回來看一下我們原來的book/views.py

1
2
3
4
5
6
7
from django.http import HttpResponse

def index(request):
  return HttpResponse("Hello BookStore!")

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

接著我們把原本的HttpResponse改寫成:

1
2
3
4
5
6
7
8
from django.http import HttpResponse
from django.shortcuts import render_to_response

def index(request):
  return render_to_response('book/index.html')

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

如果你有注意到shortcuts字樣,應該不難猜得出來因為這個功能太常用了,所以本來其實要寫一堆東西(例如要用loader把template抓出來,再把結果render出來),變成一行render_to_response就搞定,我很偷懶,所以平常幾乎都是用render_to_response就夠了。

你可能會好奇,這個book/index.html要放在哪裡? 先等等,我們先改個設定,請打開專案根目錄的settings.py,在TEMPLATE_DIRS裡加一些東西:

1
2
3
4
5
6
TEMPLATE_DIRS = (
    'templates',
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

設定這個的目的是要告訴Django要去哪邊取得template的檔案,這裡我設定了templates。接著,我們要來自己在專案根目錄底下建立一個templates的資料夾,然後結構像這樣:

image

原來的這行程式碼:

return render_to_response('book/index.html')

就會知道去哪邊找index.html這個檔案了。

這樣一來,你從設計那邊拿到的HTML檔案,就可以依照像index.html這樣的規則來擺放了。

Template API

前面提到,在Template裡不可以放Python的程式碼,只能放Template提到的API。什麼是Template API? 我直接從Django官網文件偷一小段程式碼來看:

Template API Demo (template_api_demo.py) download
1
2
3
4
5
6
7
8
9
{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

你看到的這些程式碼,是可以寫在.html裡的,反正到時候Django會把.html當做Template讀出來之後再解析它,最後把內容render給使用者看。

if .. else ..來說,因為在HTML裡沒辦法對齊(就算對齊了萬一被設計師拿回去改過回來可能又不齊了),所以Template API要用一個結束標記,告訴Django說這個if已經在這裡結尾了。

在Template裡,如果看到{% .. %}的,表示它是一個邏輯運算或是語法,通常不會有輸出值;而看到{{ .. }}的話,就是會把裡面的值給吐出來餵在HTML裡的。

Template API很多,在繼續往下之後,請你先看一下https://docs.djangoproject.com/en/1.3/ref/templates/api/這份資料,知道一下Template API是怎麼一回事,又跟一般的Python程式碼有什麼不同?

大概看過之後,接著我們來看看怎麼樣把資料給丟到Template裡呈現出來。

網址設定

Model的部份看過之後,接著介紹Django很重要的一個單元,就是怎麼在Django裡設定或設計你網站內的網址。

以前你可以會看到像這樣的網址:

http://www.eddie.com.tw/book/index.php?id=100

網址後面接問號又接了一些參數,從網址大概猜得出來這應該是編號(id)第100號的書。這樣的做法不是不好,只是近年來有更好的做法,而且對SEO(Search Engine Optimization)有一些加分的效果:

http://www.eddie.com.tw/book/100

這該怎麼做呢? 在Django的網址設定是交給一個叫做urls.py的檔案在管理(其實不限定只有一個,也可以多個/模組化的設定),先看一下它原來的樣子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.conf.urls.defaults import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
  # Examples:
  # url(r'^$', 'bookstore.views.home', name='home'),
  # url(r'^bookstore/', include('bookstore.foo.urls')),

  # Uncomment the admin/doc line below to enable admin documentation:
  # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

  # Uncomment the next line to enable the admin:
  url(r'^admin/', include(admin.site.urls)),
)

urls.py的主要功能,就是設定"當遇到某個網址的request的時候,請找指定的App裡的View的action處理"。設定網址的對應,可能會需要學一些簡單的Regular Expression語法。

我們先來設定一下整個網站的首頁,讓它交給books/views.py裡的index方法處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.conf.urls.defaults import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    url(r'^$', 'bookstore.book.views.index'),
    url(r'^books/(?P<book_id>\d+)/$', 'bookstore.book.views.detail'),
    # url(r'^bookstore/', include('bookstore.foo.urls')),

    # Uncomment the admin/doc line below to enable admin documentation:
    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    url(r'^admin/', include(admin.site.urls)),
)

其中這行:

url(r'^$', 'bookstore.book.views.index'),

表示「當遇到網址後面沒接東西的時候(也就是大家俗稱的"首頁"),請找book/views.py裡面的index方法處理」,前面的bookstore是整個專案的名稱。設定完之後如果你直接連網址http://127.0.0.1:8000/的話,會看到這個畫面:

image

跳出了ViewDoesNotExist的錯誤訊息了。看到錯誤訊息不用擔心,通常錯誤訊息裡都會跟你說哪邊有錯。原來是我們剛剛把首頁指向之後,但並沒有在對應的地方加上index方法,所以請打開book/views.py,加上index方法:

1
2
3
4
5
6
7
from django.http import HttpResponse

def index(request):
  return HttpResponse("Hello BookStore!")

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

存檔後再回來看,你應該就可以看到這個畫面:

image

如果你這樣設定:

url(r'books\.php', 'bookstore.book.views.index'),

這表示當你連到http://127.0.0.1:8000/books.php的時候,會把request丟給book/views.pyindex方法處理。看起來好像是PHP,但其實不是。二、三年前我就曾經用Django做過幫Microsoft做過某個網路行銷活動案,客戶沒有規定要用什麼程式語言(只要會動就好),不過為了讓客戶覺得我是用ASP.NET開發的,我還特地在urls.py加上.aspx的附檔名。

是有點蠢,不過也挺有趣的不是嗎? :)

接下來,我們再來看看這行:

url(r'^books/(?P<book_id>\d+)/$', 'bookstore.book.views.detail'),

意思就是說當遇到http://127.0.0.1:8000/books/100的網址的時候,那個100會被當做book_id傳給book/views.pydetail方法當做參數。

views.py裡的方法,通常我們會稱它叫做action,每個action方法的第一個參數,一定都是http request,每個action的回傳值,一定要是一個HttpResponseviews.py在一般的MVC架構裡,大概就是Controller之類的角色。

如果你有注意到在urls.py裡的這行註解,長得跟我們寫的不太一樣:

url(r'^bookstore/', include('bookstore.foo.urls')),

這其實是另一種url的設定,雞蛋不一定要放在同一個籠子裡。當你的專案如果小的時候,全部放在專案根目錄的urls.py是沒問題的(中央集權),但隨著功能越加越多,你的網址對應可能會越來越多、越來越複雜,這時候你可以在App裡自己建一個urls.py,然後把所有跟這個App有關的url"委任"給它管理(地方自治),不過這部份的細節就不再多說明,還請再參考官網文件URL dispatcher

即使我們會設定網址了,會寫對應的action了,但還是少了什麼.. 如果設計師拿一個切好版的HTML + CSS給我,難到我要像這樣用HttpResponse整個把它印出來? 當然不是,接著我們來看看在Django的MTV架構裡的T,Template!

資料表的關連

我們在開發資料庫相關的網站的時候,通常會用到許多資料表,而且這些資料表之間彼此都有關連性,接著我們就來看看在Django的Model要怎麼實作。

我們現在再來新增一個App:

> python manage.py startapp book

book/models.py的內容長得像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import models
from author.models import Profile
from django.contrib import admin

class Book(models.Model):
  title        = models.CharField(max_length = 150)
  page         = models.IntegerField()
  price        = models.IntegerField()
  author       = models.ForeignKey(Profile)
  publish_date = models.DateField()

  class Meta(object):
    db_table = "book"

class BookAdmin(admin.ModelAdmin):
  pass

admin.site.register(Book, BookAdmin)

這裡用到了ForeignKey,對應到了我們之前建的Profile類別,表示這本書的作者資料是指向profile的某一筆資料,書對作者來說是一個一對一的關係,但是一個作者可能會寫多本書,作者對書來說就是多對一的關係(雖然現實上一本書可能有超過一個以上的作者,不過我們就把它單純化,讓生活暫時美好一下..)

別忘了把book這個App也設定到settings.pyINSTALLED_APP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'author',
    'book',
)

執行python manage.py sql book,看一下它轉換出來的SQL語法:

1
2
3
4
5
6
7
8
9
10
11
BEGIN;
CREATE TABLE "book" (
    "id" integer NOT NULL PRIMARY KEY,
    "title" varchar(150) NOT NULL,
    "page" integer NOT NULL,
    "price" integer NOT NULL,
    "author_id" integer NOT NULL REFERENCES "profile" ("id"),
    "publish_date" date NOT NULL
)
;
COMMIT;

執行python manage.py syncdb,把這個Book類別也轉成資料表。因為我們剛才也把Book類別也註冊到Admin模組了,所以我們到後台也可以看得到它。你可以在新增Book資料時候發現它的Author欄位在後台變成下拉選單了:

image

方便吧 :)

Model其實還有很多東西可以介紹的,不過因為我們的目的是為了快速看過一遍Django這個web framework大概是怎麼回事,所以細節還請各位參考官網文件(一定要看!!)。

在Django裡做Database Migration

在Django新增Model是很容易的,只要建立一個繼承自models.Model的類別就行了。但比較麻煩的問題是,如果在syncdb之後還需要調整欄位的話,即使在修改Model屬性再做一次syncdb,會發現Django對於已經sync過的Model會忽略掉。所以如果在syncdb之後要再新增/修改/刪除欄位的話,大致上有兩種做法:

  1. 砍掉重練,砍掉之後再重新syncdb。
  2. 手動修改資料庫的欄位以符合Model的屬性。

如果在開發初期,第一種做法還滿簡單的。但萬一已經是在線上運作的系統就不能用這種方式;第二種做法雖然可行,但每次都得手動改,其實也滿麻煩的。

這時候,如果可以有資料庫的Migration功能就很方便了。為什麼資料庫需要做Migration? 直接修改資料庫欄位有什麼不好? 一來手工修改麻煩,二來如果同時有多人一起開發的時候,你手動修改了資料庫欄位,可能會造成別人寫的程式出現錯誤。最大的好處是,有了DB Migration,資料庫也可以做版本控制了。

在Ruby on Rails有內建的DB Migration的機制,在Django並沒有,不過可以透過另外的模組:south,來完成這個功能。

安裝

如果你有apt-get可以用:

> apt-get install python-django-south

或是使用easy_install來安裝:

> easy_install South

或是到這裡,直接下載整個壓縮檔,解壓縮之後進到該目錄裡,執行:

> python setup.py install


請注意,使用apt-get或是easy_install可能都會需要root權限!


安裝完成之後,請把south加入到專案的INSTALLED_APPS裡:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'author',
    'south',
)

再來請執行一次syncdb,Django會幫你產生一個資料表south_migrationhistory,用來紀錄Migration的歷史紀錄:

> python manage.py syncdb
Syncing...
Creating tables ...
Creating table south_migrationhistory
Installing custom SQL ...
Installing indexes ...
No fixtures found.

Synced:
 > django.contrib.auth
 > django.contrib.contenttypes
 > django.contrib.sessions
 > django.contrib.sites
 > django.contrib.messages
 > django.contrib.staticfiles
 > django.contrib.admin
 > author
 > south

Not synced (use migrations):

使用

如果要把之前建好的author這個app加進Migration裡:

> python manage.py convert_to_south author

然後你會發現在author資料夾裡面多了一個叫做migrations的目錄,裡面放了一個0001_initial.py

接下來,當你在Model做任何欄位修改之後,只要執行這行:

> python manage.py schemamigration author add_mobile_column --auto

它就會幫你找出你做了哪些欄位上的修改。如果你的欄位沒有指定預設值並且不允許NULL的話,過程中south會問你一些問題,請你指定預設值給它。順利的話,它應該會在migrations資料夾底下產生一個0002_add_mobile_column.py

但做到這裡並還沒有正式的修改到資料庫,接下來請執行python manage.py migrate

> python manage.py migrate
Running migrations for author:
 - Migrating forwards to 0002_add_mobile_column.
 > author:0002_add_mobile_column
 - Loading initial data for author.
No fixtures found.

這樣就會把剛剛做的異動正式寫入資料庫了。

回復

前面提到,資料庫有做Migration最大的好處就是隨時可以回到某一個版本,例如我們想要回到最一開始的版本的話:

> python manage.py migrate author 0001
 - Soft matched migration 0001 to 0001_initial.
Running migrations for author:
 - Migrating backwards to just after 0001_initial.
 < author:0003_change_table_name
 < author:0002_add_mobile_column

這樣就搞定了,跟手動修改資料欄位比較起來,方便許多。