about 4 years ago

  • 建置應用
    • 安裝應用
    • 移動模版與視圖
    • 重新設定urls.py
  • Django模型
    • 建立模型
    • 檢查資料(1.6.5 & 1.7)
    • 建立migration資料檔(1.7)
    • 模型與資料庫之同步
    • 模型與資料庫之同步-利用migrate(1.7)
    • 資料操作
      • 建立一個模型物件
      • 將模型物件(資料)寫入資料庫
      • 取出資料表中的資料
      • 資料欄位的顯示
      • 資料查詢與查詢集(QuerySet)
        • 全查詢
        • 過濾查詢
        • 排序查詢
        • 查詢集的操作與連鎖查詢
        • 外鍵與跨模型查詢
        • 更新與刪除資料庫數據
  • 完成更強大的餐單顯示app

還記得我們的菜單嗎!本篇要講的就是如何使用資料庫儲存web中所需要用到的資料,並且透過Django的模型輕鬆的存取。有鑑於我們的功能和架構越來越龐大,光使用一個專案來管理顯得有些不足(尤其在更大一點的case中更會發現這點),所以在正式開始之前,我們先利用manage.py來建置一個app:restaurants。

為啥app要取做restaurants呢?那是因為我們的野心很大,我們想要能夠在我們的web應用中管理多家餐廳的菜單!

建置應用

在Django的世界裡,有兩個不同層級的架構,一個是專案(project),一個是應用程式(app),專案本身包含了Django的操作命令搞manage.py和一個主要目錄(包括設定檔跟根URL設定等),我們的確可以只利用project就完成我們的網站應用,但大多數的時候,我們需要app,在第一篇裡我們提到app是一個可插拔的元件,Django中有內建了許多方便的app,而我們也可以自行建立app,不僅可供當下的專案使用,之後也可安裝到任何一個需要他的專案裡。建置app的方式如下

$ python manage.py startapp restaurants

然後我們來檢視一下現在整個專案的結構:

mysite-|--manage.py
       |--templates--math.html
       |--mysite(子結構省略)
       |--restaurants--|--__init__.py
                       |--admin.py
                       |--models.py
                       |--tests.py
                       |--views.py
                       (以下是"Django1.7"版後會增加的內容)
                       |--migrations--__init__.py
                       

各檔案說明如下:

檔案 描述
__init.py__ 一個空檔,但使得該目錄成為一個Python package(所以一個app其實就是一個python package)
admin.py 若使用Django管理後台的話,可在此註冊模型
models.py application中的模型檔
tests.py application中的測試可寫在此
views.py application中的視圖檔

[Django1.7] 1.7版中在app底下會產生一個叫做migration的目錄(package),主要放置資料庫migration的記錄

前面幾篇為了專注在學習的重點,我們並沒有建置app,現在要麻煩大家進行一些檔案的更動與設定。

安裝應用

當我們需要為一個專案插入一個應用時,只要打開專案的settings.py,在INSTALLED_APPS元組中加入application的名稱:

mysite/mysite/settings.py
...
INSTALLED_APPS = (
    #'django.contrib.admin',

    #'django.contrib.auth',

    #'django.contrib.contenttypes',

    #'django.contrib.sessions',

    #'django.contrib.messages',

    #'django.contrib.staticfiles',

    'restaurants',
)
...

其他的內建app我們可以暫且註解掉,等我們介紹到之後的應用時,再把他們打開。

移動模版與視圖

接著,我們在app的目錄下新增一個專屬於此app的模版目錄並將menu.html移到此處。

這裡會有讀者想問,我們需要在增加新模版目錄的路徑到settings.py裡面嗎?答案是不用,因為Django是默認application目錄下的templates目錄為可用的模版路徑。

接著我們來移動視圖,將menu這個視圖函數抄到restaurants下面的views.py,別忘了render_to_response等相關函數的匯入喔。我們還是列出來避免初學者有困難:

mysite/restaurants/views.py
# -*- coding: utf-8 -*-

from django.shortcuts import render_to_response

def menu(request):
    food1 = { 'name':'番茄炒蛋', 'price':60, 'comment':'好吃', 'is_spicy':False }
    food2 = { 'name':'蒜泥白肉', 'price':100, 'comment':'人氣推薦', 'is_spicy':True }
    foods = [food1,food2]
    return render_to_response('menu.html',locals())

重新設定urls.py

最後我們要重新設定對應表,其實我們對應目前還沒有更動,但要記得我們要改從restaurants的view中匯入menu函數。

mysite/mysite/urls.py
# 以上略...

from restaurants.views import menu

urlpatterns = patterns('',
    url(r'^menu/$', menu),
)

然後我們重啟我們的server,well done!一切如常。

Django模型

為了使用資料庫來管理資料,我們透過Django的模型來完成與資料庫的互動,為了專注在Django的資料操作上,我們略去了設定資料庫的程序,而直接使sqlite來進行示範。(就是說作者可以偷懶略過這邊啦)

建立模型

python的模型是一種ORM(object-relational mapping)的機制,我們透過操作我們熟悉的python類型與物件就能建立資料表和存取資料。這代表我們幾乎不需要去寫討厭的SQL語言,至少至少你不需要在兩種語言中切換。那我們開始撰寫我們的模型檔models.py

mysite/restaurants/models.py
from django.db import models

class Restaurant(models.Model):
    name = models.CharField(max_length=20)
    phone_number = models.CharField(max_length=15)
    address = models.CharField(max_length=50, blank=True)
    
class Food(models.Model):
    name = models.CharField(max_length=20)
    price = models.DecimalField(max_digits=3,decimal_places=0)
    comment = models.CharField(max_length=50, blank=True)
    is_spicy = models.BooleanField()
    restaurant = models.ForeignKey(Restaurant)

所有的模型都是繼承自django.db.models.Model的類別(class),在之後我們會看到一個模型類別會對應到資料庫中的一張資料表,而實體化自此類別的物件就是資料表中的一筆一筆資料。

上述代碼中Restaurant類別中的name,phone_numberaddress變數,會成為資料表中的欄位,這三個變數都是models中的Field物件,CharField是文字類別的欄位物件,會對應到資料表中的文字資料。值得一提的是,透過一些參數我們能指定資料庫中欄位的特性,包括max_length限制了文字的長度和blank允許了資料留空。

第二個Food類別中我們使用到了DecimalField,這其實支援了浮點數的資料,但在台灣的價格都是整數,所以我們把decimal_places設為0。BooleanField自然就是真假值的資料,正好適合is_spicy。

至於Django到底是怎麼進行對應的呢?這一切都寫在django.db.models.Model中了,我們不必擔心,簡單的來說,Django會把這些model的設定翻譯成各種資料庫的語言,這就好像是使用python對sql等語法做一個包覆(wrapper)而已,不過對於使用者來說,會用就好。這邊我們要特別探討的是

restaurant = models.ForeignKey(Restaurant)

這一行,這中間包含了重要的資料表關聯性的概念。我們知道,餐廳是一個資料表,食物(菜餚)也是,但這裡面存在著一個關係,一家餐廳有多種食物,而每個食物都只屬於一家餐廳。這屬於一種Many-to-one的關係(多種食物會對應到一家餐廳),這時我們需要使用ForeignKey(外鍵)來描述,這會使得每一個Restaurant能夠找到他所屬的所有食物,同樣的每一種Food也可以找到他屬於的餐廳。

好了,我知道你要吐槽說番茄炒蛋可能兩家餐廳都會做呢?這的確是事實,但是他們可能口味不同、價錢不同甚至說明也不同,其實這都要看當下情況而定,在我們要實作的app中,店家A的番茄炒蛋跟店家B的番茄炒蛋是不同的兩種食物,所以這邊我們才說每個食物都只屬於一家餐廳!往後你也可能碰到需要將同名食物看做同一筆資料的狀況,那時你必須改變你對模型間關聯的描述。

問題二,怎麼沒有主鍵!我想設id啦!,我想說的是,Django以id預設遞增1的方式默默的幫你設定完了,很棒吧^^

檢查資料

只有模型沒有資料那怎麼行,在輸入我們的資料之前我們先把資料表真的產生吧!我想讀者不會天真的以為剛剛的模型寫好我們就有資料表可以用了吧,還沒翻譯呢。

這個翻譯成資料庫語言並且真正執行創建資料表的動作要由manage.py來完成,在真的建置前我們先來檢查我們模型有沒有問題

$ python manage.py validate
0 errors found

[Django1.7] 1.7版之後請用python manage.py check來取代python manage.py validate,詳細的內容可以參見官網資料: System check framework

如果模型編寫無誤,我們會看到0 errors found的通知,有錯的話validate會指出錯誤所在。如果使用者真的很有興趣知道翻譯的結果,我們可以用

$ python manage.py sqlall restaurants

[Django1.7] 1.7版之後請用python manage.py sqlmigrate,詳見下一節。

然後你可以看到會被譯成的資料庫語言,如下:

BEGIN;
CREATE TABLE "restaurants_restaurant" (
    "id" integer NOT NULL PRIMARY KEY,
    "name" varchar(20) NOT NULL,
    "phone_number" varchar(15) NOT NULL,
    "address" varchar(50) NOT NULL
)
;
CREATE TABLE "restaurants_food" (
    "id" integer NOT NULL PRIMARY KEY,
    "name" varchar(20) NOT NULL,
    "price" decimal NOT NULL,
    "comment" varchar(50) NOT NULL,
    "is_spicy" bool NOT NULL,
    "restaurant_id" integer NOT NULL REFERENCES "restaurants_restaurant" ("id")
)
;
CREATE INDEX "restaurants_food_be4c8f84" ON "restaurants_food" ("restaurant_id");

COMMIT;

我想...你會喜歡用模型的XD

建立migration資料檔

若讀者是1.7版本以前的使用者,可以略過本節

在Django1.7版本之後,migrate技術已經成為Django的內定技術之一了,這由1.7版本中的app預設內建migration資料夾便可見一般。migrate技術之所以重要,在於他解決了資料庫架構更動的問題(這我們在下一節提會提及),目前我們先來了解如何使用migration。

當我們建立好資料庫模型後,我們必須針對目前的模型先建立一個migration檔,方法如下:

$ python manage.py makemigrations restaurants
                                  -----------
                                   APP_NAME

這會對指定的應用做migration的檢查,如果模型有任何異動,則會產生新的migration檔,並放置在APP底下的migration資料夾;若不提供參數APP_NAME,則Django會對所有安裝好的APP做migration的檢查。

如果模型正確且有異動(可能是新生成,增加欄位,減少欄位或被刪除),Django會列出產生的migration資訊如下:

Migrations for 'restaurants':
  0001_initial.py:
    - Create model Food
    - Create model Restaurant
    - Add field restaurant to food

首先會寫出這次的migration(異動)是發生在哪個應用上,其次會列出建立的migration檔和該檔之內容,包括建立了兩個model和增加了一個欄位(這個欄位是描述上述兩個model關係的欄位)。

另外我們注意到migration檔案的名字是0001_initial.py,這代表了資料庫模型的第一版(0001),底線之後是該次異動的說明,initial代表的是初次生成,migration檔之格式整理如下:

 XXXX_initial.py
   |       \
版本編號  異動說明

一個migration檔其實只是一個python module,主要由一個繼承自django.db.migrations.Migration的class構成,裡面包含了dependenciesoperations兩個list:

  • dependencies: 描述migration檔案的相依性,也就是說,這次的migration是基於哪一個migraiton在更動的。
  • operations: 描述了要在基礎上再進行哪些更動。

建議讀者們可以開啟該檔來看看。
如果大家不了解,我們來試試看,我們打開models.py將Restaurant模型的address欄位註解掉。並且在run一次:

$ python manage.py makemigrations restaurants

會看到結果:

Migrations for 'restaurants':
  0002_remove_restaurant_address.py:
    - Remove field address from restaurant

我們發現,這次的異動產出了0002號的migration記錄,內容是restaurant模型裡的address欄位被移除。

打開0002_remove_restaurant_address.py

mysite/restaurants/migration/0002_remove_restaurant_address.py
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
from django.db import models, migrations

class Migration(migrations.Migration):

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

    operations = [
        migrations.RemoveField(
            model_name='restaurant',
            name='address',
        ),
    ]

首先我們看到dependencies裡面有了內容,意思是這次的異動是根據0001_initial這個檔案來進行的。
其次operations標明了實際的異動,就是要刪除restaurant模型的address欄位。

我們舉個簡單的例子來說,原本我們有變數x,其值為5,我們對這個狀態進行一次記錄(類似makemigrations),產生了0001號記錄檔,接著我們將x+2,再做一次記錄得到0002號檔,0002號檔裡面記錄了兩樣東西,一個是他是根據0001號檔來改的(dependencies),第二個是記錄要加上2(operations),所以之後我們只要確認0001號檔的改變已經生效(x=5了),就可以放心的套用0002號檔的動作+2,來完成最後的更動。

在本節的最後,我們來介紹一下如何在1.7之後的版本看到sql語言的翻譯,那就是用:

$ python manage.py sqlmigrate restaurants 0002
                              ----------- ----
                               APP_NAME  版本編號

我們可以看到他做了什麼:

BEGIN;
CREATE TABLE "restaurants_restaurant__new" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "name" varchar(20) NOT NULL,
    "phone_number" varchar(15) NOT NULL);
INSERT INTO "restaurants_restaurant__new" (
    "phone_number", "id", "name") 
    SELECT "phone_number", "id", "name" FROM "restaurants_restaurant";
DROP TABLE "restaurants_restaurant";
ALTER TABLE "restaurants_restaurant__new" RENAME TO "restaurants_restaurant";
COMMIT;

好意提醒:try完了記得將address欄位的註解取消唷!

模型與資料庫之同步

若是1.7版的使用者請略過本節閱讀下一節。

好的,說了那麼多,資料表就給他真的建出來吧!

$ python manage.py syncdb

然後你可以看到生成資料表的一些資訊

Creating tables ...
Creating table restaurants_restaurant
Creating table restaurants_food

我們可以觀察到syncdb這個動作使得Django去讀取我們寫的模型,並將資料表產生。沒錯!就是只要syncdb,資料表就會跑出來了,真是太神啦。不過如果讀者想要修改資料表的結構(這裡是說結構不是內容喔),可就不是那麼容易了,syncdb沒有辦法將我們編輯或刪除模型的動作同步到資料庫中,大家應當要審慎的規劃資料庫結構,免得做了白工。

若有意外或是不得已的地方,相關的解決方案有3:

  • 手動修改資料庫
  • 使用south
  • 使用Django1.7之後的版本(利用migration)

模型與資料庫之同步-利用migrate

1.7版本以前的讀者請略過本節。

使用migrate技術的1.7版,在同步資料庫這一塊可以做的更好,對於1.7以前版本所不支援的資料庫異動,這裡通通都能輕鬆完成。首先我們要介紹migrate這個指令:

$ python manage.py migrate restaurants 0001
                          ------------ ----
                            APP_NAME  版本編號

migrate會根據指定的migration記錄(利用編號指定)去將模型同步到資料庫,只要使用了migrate指令,就可以根據模型異動,將現行的資料庫調整到與記錄檔一樣。若不指定編號,則自動更新到最新版本。
另外,與makemigrations相同,migrate需要指定一個應用,不然會對所有的APP進行更動。

執行完應該會有以下結果:

Operations to perform:
  Target specific migration: 0001_initial, from restaurants
Running migrations:
  Applying restaurants.0001_initial... OK

我們會發現使用migrate的好處是我們可以任意地更動資料庫結構並作成各種版本的記錄,且隨時可以指定任一版本進行同步。

資料操作

接著我們來進行一些資料的操作吧,打開我們親愛的Django shell:

這個部分非常重要,雖然稍後我們會看到對於設定資料庫我們有更友善的介面(admin interface),但若我們想要讓使用者可以參與資料庫資料的新增、刪除或編輯,這些操作的手法會十分重要。

$ python manage.py shell

試著執行以下的動作:

建立一個模型物件

>>> from restaurants.models import Restaurant, Food
>>> r1 = Restaurant(name='派森家常小館', phone_number='02-12345678', address='天龍國天龍區天龍路1號')
>>> r1
<Restaurant: Restaurant object>

建立一個模型物件很簡單,記得匯入app中models.py裡的模型,並以參數(模型中個資料欄位的名稱)將之實體化即可,如上例我們觀察到r1是一個Restaurant物件,也就是資料表中的一筆資料,不過切記,目前該筆資料尚未被寫入資料庫中。

將模型物件(資料)寫入資料庫

接著我們利用save方法可以真的將資料寫入資料庫,等我們關閉shell後,該筆資料仍然會存在:

>>> r1.save()

不過有的時候我們會希望一次完成模型資料的建立與寫入資料庫,模型的objects.create方法便提供了一步完成的捷徑:

>>> r2 = Restaurant.objects.create(name='古意得餐廳', phone_number='02-7654321', address='天龍國天龍區天龍路100號')
>>> r2
<Restaurant: Restaurant object>

我們對於r1和r2在shell中的顯示有那麼一點點的不滿意,兩家不同的餐廳顯示出來的資訊都一樣,這有時候對於使用者要檢視或分辨資料時會有困難,我們可以利用python中物件的小技巧,覆寫物件中的__unicode__方法即可,讓我們打開models.py:

mysite/restaurants/models.py
from django.db import models

class Restaurant(models.Model):
    name = models.CharField(max_length=20)
    phone_number = models.CharField(max_length=15)
    address = models.CharField(max_length=50, blank=True)

    def __unicode__(self):
        return self.name
    
class Food(models.Model):
    name = models.CharField(max_length=20)
    price = models.DecimalField(max_digits=3,decimal_places=0)
    comment = models.CharField(max_length=50, blank=True)
    is_spicy = models.BooleanField()
    restaurant = models.ForeignKey(Restaurant)
    
    def __unicode__(self):
        return self.name

此時我們也順便修改了Food的顯示方式,以便以後使用。這樣設定的好處我們將在稍後演示。

取出資料表中的資料

接著讓我們先離開shell,再重新進入,這是為了讓我們剛剛修改過的模型代碼可以被載入直譯器。好了,剛剛的物件不存在了,畢竟我們離開了shell,不過沒關係,資料都在資料庫裡呢,我們只要用Django模型的操作手法就可以再度拿出資料囉(你該不會以為要用SQL了吧!)

但我們還是得進行一次匯入:

>>> from restaurants.models import Restaurant, Food
>>> restaurants = Restaurant.objects.all()
>>> restaurants
[<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>]

這邊有兩個值得注意的點,第一個是我們知道了利用模型的objects.all()方法可以從模型對應的資料表中取得所有的資料,並會回傳一個對應的模型物件清單(list of model objects);第二個是我們發現模型物件在shell中有更具有表述例的顯示了,這多虧了__unicode__方法。

資料欄位的顯示

那我們要如何取得每一筆資料的欄位值呢?這很直覺,利用物件屬性的存取就行了,Django模型厲害的地方就在這裡,你只需要利用python的方式思考,基本的操作就可以完成幾乎所有的功能了。

>>> r1 = restaurants[0]
>>> r1.phone_number
u'02-12345678'
>>> r1.id
1

這裡我們會發現我們拿到的都是unicode字串,這點對於非英語系國家的使用者來說無疑是一大福音。我們也可以看到自動設置的id屬性值也可以拿的到。

如果你顯示餐廳名字的時候被長得像亂碼的東西嚇到,別緊張,那只是因為他用了unicode編碼,試試看用print r1.name,一切正常!

資料查詢與查詢集(QuerySet)

正如同我們利用SQL語法,我們也要會從資料庫中查詢出資料,我們先從取出一筆資料開始:

>>> r = Restaurant.objects.get(name='古意得餐廳')
>>> r
<Restaurant: 古意得餐廳>

objects.get方法會回傳一個模型物件,也就是一筆資料,如果回傳的是多筆或是查詢結果失敗(空),都會引發例外,這可以利用try/expect來捕獲並處理。

接著我們來看看已經使用過的全查詢:

全查詢
>>> restaurants = Restaurant.objects.all()
[<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>]

這將會取得資料表中的所有資料(模型中的所有物件),這邊我們要定義幾個名詞,objects稱之為管理器,每個模型都有一個管理器,他包含了關於查詢該模型資料的種種方法。而回傳出來的這個資料清單(嚴格來說不是我們所熟知的那個清單),被稱為QuerySet:查詢集,他是一個類似於清單的物件,你就想像成他是儲存著藉由管理器查詢回來的資料集合就好。

對於查詢集我們可以使用類似清單的方式來取值或做slicing(切片):

>>> restaurants = Restaurant.objects.all()
>>> restaurants[0]
<Restaurant: 派森家常小館>
>>> restaurants[0:2]
[<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>]
>>> restaurants[-1]
AssertionError: Negative indexing is not supported.   # 可惜並不支援負向索引

接下來我們會陸續介紹幾種會返回查詢集的查詢方法。

過濾查詢

利用objects管理器的filter方法,可以對於指定的資料欄位進行過濾:

>>> restaurants = Restaurant.objects.filter(name='古意得餐廳')
[<Restaurant: 古意得餐廳>]

過濾還能使用多重過濾:

>>> restaurants = Restaurant.objects.filter(name='古意得餐廳', phone_number='02-7654321')
[<Restaurant: 古意得餐廳>]

或是包含過濾:

>>> restaurants = Restaurant.objects.filter(name__contains='餐廳')
[<Restaurant: 古意得餐廳>]

利用屬性名+__contains=值可以搜尋該屬性包含值的資料(包含的意思表示不需全等,部分有符合即可),這不只可以用在過濾,所有查詢方法的屬性都可以使用這招來讓查詢的彈性更佳。

排序查詢

為了能夠展示接下來的查詢功能,我們開始產生幾筆Food資料:

>>> r = Restaurant.objects.get(name='古意得餐廳')
>>> f1 = Food(name='宮保雞丁', price=120, comment='超級辣', is_spicy=True, restaurant = r)
>>> f1.save()
>>> f2 = Food(name='炒青菜', price=85, comment='每日不同', is_spicy=False, restaurant = r)
>>> f2.save()
>>> Food.objects.all()
[<Food: 宮保雞丁>, <Food: 炒青菜>]

我們會發現查詢出來的QuerySet是依照id排序的,利用objects管理器的order_by方法,可以自行指定資料欄位進行排序:

>>> Food.objects.all()
[<Food: 宮保雞丁>, <Food: 炒青菜>]
>>> Food.objects.order_by('price')
[<Food: 炒青菜>, <Food: 宮保雞丁>]

如此一來我們便可以得到一個價位由低往高的QuerySet了,這邊有個錯誤很容易犯,order_by的參數是字串,不要誤植為order_by(price)了。另外,多重排序跟反向排序對我們也很有幫助(能處理反向的地方很多,看讀者偏好哪種):

>>> Food.objects.order_by('price','name')      # 先排price再排name

[<Food: 炒青菜>, <Food: 宮保雞丁>]
>>> Food.objects.order_by('name','price')      # 先排name再排price

[<Food: 宮保雞丁>, <Food: 炒青菜>]
>>> Food.objects.order_by('-price')            # 反向排序

[<Food: 宮保雞丁>, <Food: 炒青菜>]

還有一點值得一提,如果有某項特性是排序的優先選擇,導致我們每次都要使用order_by來排序,為了避免麻煩可使用內嵌的Meta class,若我們沒有特別使用order_by,Food的資料查詢總會以'price'為優先序,如下範例:

mysite/restaurants/models.py
# 以上略...

class Food(models.Model):
    name = models.CharField(max_length=20)
    price = models.DecimalField(max_digits=3,decimal_places=0)
    comment = models.CharField(max_length=50, blank=True)
    is_spicy = models.BooleanField()
    restaurant = models.ForeignKey(Restaurant)

    def __unicode__(self):
        return self.name

    class Meta:
        ordering = ['price']
查詢集的操作與連鎖查詢

任何一個QuerySet都能夠繼續以order_byfilterget等方法進行查詢:

>>> foods = Food.objects.order_by('price')       # foods = [<Food: 炒青菜>, <Food: 宮保雞丁>]

>>> foods = foods.filter(is_spicy=True)          # foods = [<Food: 宮保雞丁>]

>>> food = foods.get(name__contains='宮保')
>>> food
<Food: 宮保雞丁>

上述操作可做一個串連:

>>> Food.objects.order_by('price').filter(is_spicy=True).get(name__contains='宮保')
<Food: 宮保雞丁>
外鍵與跨模型查詢

在關聯性資料庫中,兩張資料表若有對應關係(多對一,多對多等),我們必須要能從一張資料表透過關係去找到另外一張資料表中的資料,在Django中,我們要透過的是跨模型(跨資料表)的查詢,簡單的關係如多對一關係只需要利用foreign key(外鍵)即可,還記得我們的餐廳跟食物是多對一關係嗎?多種食物屬於一家餐廳或是一家餐廳有多種食物,在模型中,餐廳模型並沒有特殊的設定,但是食物模型多了一個外鍵指出我是屬於哪個餐廳的,那一筆食物資料(食物物件)要如何找到他的餐廳呢,請看範例:

>>> food = Food.objects.get(name='宮保雞丁')
>>> food.restaurant
<Restaurant: 古意得餐廳>

就是那麼簡單,他的餐廳屬性就是所屬的餐廳模型物件,酷吧,當然要查出一家餐廳擁有的菜色也很簡單:

>>> r = Restaurant.objects.get(name='古意得餐廳')
>>> r.food_set
<django.db.models.fields.related.RelatedManager at 0x1040c9390>
>>> r.food_set.all()
[<Food: 宮保雞丁>, <Food: 炒青菜>]

利用小寫模型名稱_set就可得到一個關係管理器,就類似於objects管理器,我們可以對他使用各種查詢的方法!

還有一種關係我們沒有講到,那我們便舉一個暫時的例子:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=30)
    
    def __unicode__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

有兩個模型,書與作者,一本書有一至多個作者,而一個作者有一至多本著作(書),我們稱這種關係叫做多對多關係。可以使用models.ManyToManyField來描述這個關係,一旦我們要跨資料庫查詢的時候,書本可以利用authors來找到他的作者群:

>>> book = Book.objects.get(id=1)
>>> book.authors.all()
[<Author: 金庸>, <Author: 古龍>]

而作者因為沒有book相關屬性,那就要使用book_set來查詢了

>>> author = Author.objects.get(id=1)
>>> a.book_set.all()
[<Book: 便秘的魔法石>, <Book: 考盃的考驗>]

筆者囉嗦補充一點,用id進行查詢是最準確的方式,因為他會是主鍵,在許多地方我們應該堅持使用id進行查詢,我們會在接下來的幾篇裡面看到。

更新與刪除資料庫數據

今天我們發現有一筆資料我需要進行一些修正或編輯(大概是雞價漲了),我們可以這樣做:

>>> food = Food.objects.get(name='宮保雞丁')
>>> food.price = 200
>>> food.save()

不過要注意的是,我們修改了屬性值也只是對此模型物件做了更動,要真的將更動寫入資料庫還是得用save方法,但是save方法是會將一筆資料的所有欄位重新輸入(依據當前對應的模型物件),這在一般情況當然沒問題,但是當你的應用程序有race condition的時候,可能會造成一些錯誤,為了避免此種狀況,我們得用別的方法。

在查詢集中有一個方法update可以滿足我們的需求:

>>> Food.objects.filter(name='宮保雞丁').update(price=200)
1

注意!update是QuerySet的方法,無法作用在單獨物件上,所以我們要偷偷用點技巧,使用filter來過濾出只含有宮保雞丁的QuerySet。另外,我們也能觀察到此方法會回傳一個整數代表有幾筆資料被更新了。

那既然他是查詢集的方法,就代表其實update是可以更動所有在查詢集中的物件的,比如說今天古意得餐廳改變營業模式成為熱炒100了,那我們可以寫下下列代碼:

>>> Restaurant.objects.get(name='古意得餐廳').food_set.update(price=100)
2

要刪除資料呢我們可以用delete方法,他不像update只能用在查詢集,單獨的模型物件或是查詢集都有這個方法:

>>> f = Food.objects.get(name='宮保雞丁')
>>> f.delete()
>>> Food.objects.all()
[<Food: 炒青菜>]
>>> Food.objects.all().delete()
[]

完成更強大的餐單顯示app

學了那麼多,要學以致用,我們來完善我們的顯示菜單的app吧。

因為我們多出了餐廳的模型,我們可以一次秀出所有餐廳的所有菜餚,打開restaurants/views.pyrestaurants/menu.html,我們為了顯示上的好看,利用表格取代了列表,同實作了一點點修正,希望讀者不要覺得筆者很討厭:

mysite/restaurants/views.py
from django.shortcuts import render_to_response
from restaurants.models import Restaurant, Food

def menu(request):
    restaurants = Restaurant.objects.all()
    return render_to_response('menu.html',locals())
mysite/restaurants/templates/menu.html
<!doctype html>
<html>
    <head>
        <title> Menu </title>
        <meta charset='utf-8'>
    </head>
    <body>
        {% for r in restaurants %}
            <h2>{{ r.name }}</h2>
            {% if r.food_set.all %}
                <p>本餐廳共有{{ r.food_set.all|length }}道菜</p>
                <table>
                    <tr>
                        <th>菜名</th>
                        <th>價格</th>
                        <th>註解</th>
                        <th>辣不辣</th>
                    </tr>
                {% for food in r.food_set.all %}
                    <tr>
                        <td> {{ food.name }} </td>
                        <td> {{ food.price }} </td>
                        <td> {{ food.comment }} </td>
                        <td> {% if food.is_spicy %}  {% else %} 不辣 {% endif %} </td>
                    </tr>
                {% endfor %}
                </table>
            {% else %}
                <p>本餐廳啥都沒賣</p>
            {% endif %}
        {% endfor %}
    </body>
</html>

我們來看一下成果,我們在前幾篇成功地將view logic交給了模版,也在本篇中將與資料相關的部份交給了模型與資料庫,終於,我們的視圖函式只剩business logic了,漂亮!

這種將資料分出去的方式有很多好處而且也才實際,資料在資料庫中可以永久被保存,且有良好的架構和欄位,而不是一個隨便的字典、清單或是一份文字記錄檔,各種對資料的存取建立等也都能利用模型獨立且有效率的處理。如果讀者在接下來幾篇了解了admin以及透過表單讓使用者參與資料庫的編修後,會對模型帶來的便利感到無比感動。那話不多說,先從admin開始吧。如果讀者非常不介意admin而比較care如何和使用者互動的話,就請跳到下下一篇,表單!

← Django筆記(4) - 模版的變量與標籤 Django筆記(6) - 後台管理系統Admin →
 
comments powered by Disqus