在第5章里,我們介紹了Django的數據層如何定義數據模型以及如何使用數據庫API來創建、檢索、更新以及刪除記錄 在這章里,我們將向你介紹Django在這方面的一些更高級功能。
先讓我們回憶一下在第五章里的關于書本(book)的數據模型:
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=50)
city = models.CharField(max_length=60)
state_province = models.CharField(max_length=30)
country = models.CharField(max_length=50)
website = models.URLField()
def __unicode__(self):
return self.name
class Author(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=40)
email = models.EmailField()
def __unicode__(self):
return u'%s %s' % (self.first_name, self.last_name)
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()
def __unicode__(self):
return self.title
如我們在第5章的講解,獲取數據庫對象的特定字段的值只需直接使用屬性。 例如,要確定ID為50的書本的標題,我們這樣做:
>>> from mysite.books.models import Book
>>> b = Book.objects.get(id=50)
>>> b.title
u'The Django Book'
但是,在之前有一件我們沒提及到的是表現為ForeignKey 或 ManyToManyField的關聯對象字段,它們的作用稍有不同。
當你獲取一個ForeignKey 字段時,你會得到相關的數據模型對象。 例如:
>>> b = Book.objects.get(id=50)
>>> b.publisher
>>> b.publisher.website
u'http://www.apress.com/'
對于用ForeignKey
來定義的關系來說,在關系的另一端也能反向的追溯回來,只不過由于不對稱性的關系而稍有不同。 通過一個publisher
對象,直接獲取 books ,用 publisher.book_set.all() ,如下:
>>> p = Publisher.objects.get(name='Apress Publishing')
>>> p.book_set.all()
[<Book: The Django Book>, <Book: Dive Into Python>, ...]
實際上,book_set 只是一個 QuerySet(參考第5章的介紹),所以它可以像QuerySet一樣,能實現數據過濾和分切,例如:
>>> p = Publisher.objects.get(name='Apress Publishing')
>>> p.book_set.filter(name__icontains='django')
[<Book: The Django Book>, <Book: Pro Django>]
屬性名稱book_set是由模型名稱的小寫(如book)加_set組成的。
多對多和外鍵工作方式相同,只不過我們處理的是QuerySet而不是模型實例。 例如,這里是如何查看書籍的作者:
>>> b = Book.objects.get(id=50)
>>> b.authors.all()
[<Author: Adrian Holovaty>, <Author: Jacob Kaplan-Moss>]
>>> b.authors.filter(first_name='Adrian')
[<Author: Adrian Holovaty>]
>>> b.authors.filter(first_name='Adam')
[]
反向查詢也可以。 要查看一個作者的所有書籍,使用author.book_set ,就如這樣:
>>> a = Author.objects.get(first_name='Adrian', last_name='Holovaty')
>>> a.book_set.all()
[<Book: The Django Book>, <Book: Adrian's Other Book>]
這里,就像使用 ForeignKey字段一樣,屬性名book_set是在數據模型(model)名后追加_set。
在我們在第5章介紹 syncdb 這個命令時, 我們注意到 syncdb僅僅創建數據庫里還沒有的表,它 并不 對你數據模型的修改進行同步,也不處理數據模型的刪除。 如果你新增或修改數據模型里的字段,或是刪除了一個數據模型,你需要手動在數據庫里進行相應的修改。 這段將解釋了具體怎么做:
當處理模型修改的時候,將Django的數據庫層的工作流程銘記于心是很重要的。
如果模型包含一個未曾在數據庫里建立的字段,Django會報出錯信息。 當你第一次用Django的數據庫API請求表中不存在的字段時會導致錯誤(就是說,它會在運行時出錯,而不是編譯時)。
Django_不_關心數據庫表中是否存在未在模型中定義的列。
改變模型的模式架構意味著需要按照順序更改Python代碼和數據庫。
當要向一個產品設置表(或者說是model)添加一個字段的時候,要使用的技巧是利用Django不關心表里是否包含model里所沒有的列的特性。 策略就是現在數據庫里加入字段,然后同步Django的模型以包含新字段。
然而 這里有一個雞生蛋蛋生雞的問題 ,由于要想了解新增列的SQL語句,你需要使用Django的manage.py sqlall命令進行查看 ,而這又需要字段已經在模型里存在了。 (注意:你并 _不是非得使用_與Django相同的SQL語句創建新的字段,但是這樣做確實是一個好主意 ,它能讓一切都保持同步。)
這個雞-蛋的問題的解決方法是在開發者環境里而不是發布環境里實現這個變化。 (你_正_使用的是測試/開發環境,對吧?)下面是具體的實施步驟。
首先,進入開發環境(也就是說,不是在發布環境里):
在你的模型里添加字段。
運行 manage.py sqlall [yourapp] 來測試模型新的 CREATE TABLE 語句。 注意為新字段的列定義。
開啟你的數據庫的交互命令界面(比如, psql 或mysql , 或者可以使用 manage.py dbshell )。 執行ALTER TABLE 語句來添加新列。
然后在你的產品服務器上再實施一遍這些步驟。
啟動數據庫的交互界面。
執行在開發環境步驟中,第三步的ALTER TABLE語句。
將新的字段加入到模型中。 如果你使用了某種版本控制工具,并且在第一步中,已經提交了你在開發環境上的修改,現在,可以在生產環境中更新你的代碼了(例如,如果你使用Subversion,執行svn update。
讓我們實踐下,比如添加一個num_pages字段到第五章中Book模型。首先,我們會把開發環境中的模型改成如下形式:
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()
num_pages = models.IntegerField(blank=True, null=True)
def __unicode__(self):
return self.title
(注意 閱讀第六章的“設置可選字段”以及本章下面的“添加非空列”小節以了解我們在這里添加blank=True和null=True的原因。)
然后,我們運行命令manage.py sqlall books 來查看CREATE TABLE語句。 語句的具體內容取決與你所使用的數據庫, 大概是這個樣子:
CREATE TABLE "books_book" (
"id" serial NOT NULL PRIMARY KEY,
"title" varchar(100) NOT NULL,
"publisher_id" integer NOT NULL REFERENCES "books_publisher" ("id"),
"publication_date" date NOT NULL,
"num_pages" integer NULL
);
新加的字段被這樣表示:
"num_pages" integer NULL
接下來,我們要在開發環境上運行數據庫客戶端,如果是PostgreSQL,運行 psql,,然后,我執行如下語句。
ALTER TABLE books_book ADD COLUMN num_pages integer;
添加 非NULL 字段
這里有個微妙之處值得一提。 在我們添加字段num_pages的時候,我們使用了 blank=True 和 null=True 選項。 這是因為在我們第一次創建它的時候,這個數據庫字段會含有空值。
然而,想要添加不能含有空值的字段也是可以的。 要想實現這樣的效果,你必須先創建 NULL 型的字段,然后將該字段的值填充為某個默認值,然后再將該字段改為 NOT NULL 型。 例如:
BEGIN;
ALTER TABLE books_book ADD COLUMN num_pages integer;
UPDATE books_book SET num_pages=0;
ALTER TABLE books_book ALTER COLUMN num_pages SET NOT NULL;
COMMIT;
如果你這樣做,記得你不要在模型中添加 blank=True 和 null=True 選項。
執行ALTER TABLE之后,我們要驗證一下修改結果是否正確。啟動python并執行下面的代碼:
>>> from mysite.books.models import Book
>>> Book.objects.all()[:5]
如果沒有異常發生,我們將切換到生產服務器,然后在生產環境的數據庫中執行命令ALTER TABLE 然后我們更新生產環境中的模型,最后重啟web服務器。
從Model中刪除一個字段要比添加容易得多。 刪除字段,僅僅只要以下幾個步驟:
刪除字段,然后重新啟動你的web服務器。
用以下命令從數據庫中刪除字段:
ALTER TABLE books_book DROP COLUMN num_pages;
請保證操作的順序正確。 如果你先從數據庫中刪除字段,Django將會立即拋出異常。
由于多對多關聯字段不同于普通字段,所以刪除操作是不同的。
從你的模型中刪除ManyToManyField,然后重啟web服務器。
用下面的命令從數據庫刪除關聯表:
DROP TABLE books_book_authors;
像上面一樣,注意操作的順序。
刪除整個模型要比刪除一個字段容易。 刪除一個模型只要以下幾個步驟:
從文件中刪除你想要刪除的模型,然后重啟web 服務器models.py
然后用以下命令從數據庫中刪除表:
DROP TABLE books_book;
當你需要從數據庫中刪除任何有依賴的表時要注意(也就是任何與表books_book有外鍵的表 )。
正如在前面部分,一定要按這樣的順序做。
在語句Book.objects.all()中,objects是一個特殊的屬性,需要通過它查詢數據庫。 在第5章,我們只是簡要地說這是模塊的manager 。現在是時候深入了解managers是什么和如何使用了。
總之,模塊manager是一個對象,Django模塊通過它進行數據庫查詢。 每個Django模塊至少有一個manager,你可以創建自定義manager以定制數據庫訪問。
下面是你創建自定義manager的兩個原因: 增加額外的manager方法,和/或修manager返回的初始QuerySet。
增加額外的manager方法是為模塊添加表級功能的首選辦法。 (至于行級功能,也就是只作用于模型對象實例的函數,一會兒將在本章后面解釋。)
例如,我們為Book模型定義了一個title_count()方法,它需要一個關鍵字,返回包含這個關鍵字的書的數量。 (這個例子有點牽強,不過它可以說明managers如何工作。)
# models.py
from django.db import models
# ... Author and Publisher models here ...
class BookManager(models.Manager):
def title_count(self, keyword):
return self.filter(title__icontains=keyword).count()
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()
num_pages = models.IntegerField(blank=True, null=True)
objects = BookManager()
def __unicode__(self):
return self.title
有了這個manager,我們現在可以這樣做:
>>> Book.objects.title_count('django')
4
>>> Book.objects.title_count('python')
18
下面是編碼該注意的一些地方:
我們建立了一個BookManager類,它繼承了django.db.models.Manager。這個類只有一個title_count()方法,用來做統計。 注意,這個方法使用了self.filter(),此處self指manager本身。
為什么我們要添加一個title_count()方法呢?是為了將經常使用的查詢進行封裝,這樣我們就不必重復編碼了。
manager的基本QuerySet返回系統中的所有對象。 例如,Book.objects.all()
返回數據庫book中的所有書本。
我們可以通過覆蓋Manager.get_query_set()方法來重寫manager的基本QuerySet。 get_query_set()按照你的要求返回一個QuerySet。
例如,下面的模型有 兩個 manager。一個返回所有對像,另一個只返回作者是Roald Dahl的書。
from django.db import models
# First, define the Manager subclass.
class DahlBookManager(models.Manager):
def get_query_set(self):
return super(DahlBookManager, self).get_query_set().filter(author='Roald Dahl')
# Then hook it into the Book model explicitly.
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=50)
# ...
objects = models.Manager() # The default manager.
dahl_objects = DahlBookManager() # The Dahl-specific manager.
在這個示例模型中,Book.objects.all()返回了數據庫中的所有書本,而Book.dahl_objects.all()只返回了一本. 注意我們明確地將objects設置成manager的實例,因為如果我們不這么做,那么唯一可用的manager就將是dah1_objects。
當然,由于get_query_set()返回的是一個QuerySet對象,所以我們可以使用filter(),exclude()和其他一切QuerySet的方法。 像這些語法都是正確的:
Book.dahl_objects.all()
Book.dahl_objects.filter(title='Matilda')
Book.dahl_objects.count()
這個例子也指出了其他有趣的技術: 在同一個模型中使用多個manager。 只要你愿意,你可以為你的模型添加多個manager()實例。 這是一個為模型添加通用濾器的簡單方法。
例如:
class MaleManager(models.Manager):
def get_query_set(self):
return super(MaleManager, self).get_query_set().filter(sex='M')
class FemaleManager(models.Manager):
def get_query_set(self):
return super(FemaleManager, self).get_query_set().filter(sex='F')
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female')))
people = models.Manager()
men = MaleManager()
women = FemaleManager()
這個例子允許你執行Person.men.all()
,Person.women.all()
,Person.people.all()
查詢,生成你想要的結果。
如果你使用自定義的Manager對象,請注意,Django遇到的第一個Manager(以它在模型中被定義的位置為準)會有一個特殊狀態。 Django將會把第一個Manager 定義為默認Manager ,Django的許多部分(但是不包括admin應用)將會明確地為模型使用這個manager。 結論是,你應該小心地選擇你的默認manager。因為覆蓋get_query_set() 了,你可能接受到一個無用的返回對像,你必須避免這種情況。
為了給你的對像添加一個行級功能,那就定義一個自定義方法。 有鑒于manager經常被用來用一些整表操作(table-wide),模型方法應該只對特殊模型實例起作用。
這是一項在模型的一個地方集中業務邏輯的技術。
最好用例子來解釋一下。 這個模型有一些自定義方法:
from django.contrib.localflavor.us.models import USStateField
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
birth_date = models.DateField()
address = models.CharField(max_length=100)
city = models.CharField(max_length=50)
state = USStateField() # Yes, this is U.S.-centric...
def baby_boomer_status(self):
"Returns the person's baby-boomer status."
import datetime
if datetime.date(1945, 8, 1) <= self.birth_date <= datetime.date(1964, 12, 31):
return "Baby boomer"
if self.birth_date < datetime.date(1945, 8, 1):
return "Pre-boomer"
return "Post-boomer"
def is_midwestern(self):
"Returns True if this person is from the Midwest."
return self.state in ('IL', 'WI', 'MI', 'IN', 'OH', 'IA', 'MO')
def _get_full_name(self):
"Returns the person's full name."
return u'%s %s' % (self.first_name, self.last_name)
full_name = property(_get_full_name)
例子中的最后一個方法是一個property。 想了解更多關于屬性的信息請訪問http://www.python.org/download/releases/2.2/descrintro/#property
這是用法的實例:
>>> p = Person.objects.get(first_name='Barack', last_name='Obama')
>>> p.birth_date
datetime.date(1961, 8, 4)
>>> p.baby_boomer_status()
'Baby boomer'
>>> p.is_midwestern()
True
>>> p.full_name # Note this isn't a method -- it's treated as an attribute
u'Barack Obama'
有時候你會發現Django數據庫API帶給你的也只有這么多,那你可以為你的數據庫寫一些自定義SQL查詢。 你可以通過導入django.db.connection對像來輕松實現,它代表當前數據庫連接。 要使用它,需要通過connection.cursor()得到一個游標對像。 然后,使用cursor.execute(sql, [params])來執行SQL語句,使用cursor.fetchone()或者cursor.fetchall()來返回記錄集。 例如:
>>> from django.db import connection
>>> cursor = connection.cursor()
>>> cursor.execute("""
... SELECT DISTINCT first_name
... FROM people_person
... WHERE last_name = %s""", ['Lennon'])
>>> row = cursor.fetchone()
>>> print row
['John']
connection和cursor幾乎實現了標準Python DB-API,你可以訪問 http://www.python.org/peps/pep-0249.html http://www.python.org/peps/pep-0249.html>
__來獲取更多信息。 如果你對Python DB-API不熟悉,請注意在cursor.execute() 的SQL語句中使用“%s”
,而不要在SQL內直接添加參數。 如果你使用這項技術,數據庫基礎庫將會自動添加引號,同時在必要的情況下轉意你的參數。
不要把你的視圖代碼和django.db.connection語句混雜在一起,把它們放在自定義模型或者自定義manager方法中是個不錯的主意。 比如,上面的例子可以被整合成一個自定義manager方法,就像這樣:
from django.db import connection, models
class PersonManager(models.Manager):
def first_names(self, last_name):
cursor = connection.cursor()
cursor.execute("""
SELECT DISTINCT first_name
FROM people_person
WHERE last_name = %s""", [last_name])
return [row[0] for row in cursor.fetchone()]
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
objects = PersonManager()
然后這樣使用:
>>> Person.objects.first_names('Lennon')
['John', 'Cynthia']
在下一章 我們將講解Django的通用視圖框架,使用它創建常見的網站可以節省時間。