之後的例子,都是用下面的資料模型
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def __str__(self): # __unicode__ on Python 2
return self.name
class Author(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField()
def __str__(self): # __unicode__ on Python 2
return self.name
class Entry(models.Model):
blog = models.ForeignKey(Blog, related_name='entries')
headline = models.CharField(max_length=255)
body_text = models.TextField()
pub_date = models.DateField()
mod_date = models.DateField()
authors = models.ManyToManyField(Author)
n_comments = models.IntegerField()
n_pingbacks = models.IntegerField()
rating = models.IntegerField()
def __str__(self): # __unicode__ on Python 2
return self.headline
model class → database table model instance → table record
建立物件流程:
- 初始化一個 model instance,並依需求附加關鍵字參數 (keyword arguments)。
- 呼叫 instance 的
save()
方法。
>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()
這個流程的背面提供了 INSERT
的 SQL 指令。直到呼叫 save()
,Django 才真正去對資料庫做操作。
save()
方法沒有回傳值。
參考:save()
要同時建立並儲存物件,使用 create()
方法:
>>> Blog.objects.create(name='Beatles Blog', tagline='All the latest Beatles news.')
要儲存對已存在資料庫的物件所做的修改,一樣使用 save()
。
b = Blog.objects.get(id=1)
b.name = 'New name'
b.save()
這裡的 save()
提供了 SQL 的 UPDATE
命令。同時的,直到呼叫 save()
,Django 才會對資料庫進行操作。
更新外鍵欄位的機制跟儲存一般欄位的方法一樣:指定合適的資料物件到欄位中。
>>> from blog.models import Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()
更新多對多欄位則有點不同:使用該欄位的 add()
方法來增加 record 到關係中。
>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)
要一次新增多筆多對多欄位,直接在呼叫 add()
時使用多個參數即可:
>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)
為了取出資料庫中的物件,要透過資料模型類別上的 Manager
建立一個 QuerySet
。
一個查詢集代表資料庫中部分物件的集合。它可以有零至多個過濾器。過濾器能根據提供的參數,縮小查詢的結果。以 SQL 來看,一個查詢集等於一個 SELECT
命令,而過濾器等同於 SQL 的限制句,如 WHERE
或 LIMIT
。
使用資料模型別類的 Manager
來獲得查詢集。每個 model 至少有一個 Manager
,預設名稱叫 objects
。直接透過模型類別來取用:
>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
...
AttributeError: "Manager isn't accessible via Blog instances."
Managers
只能透過 model class 存取,不能透過 model instance。這樣能將資料表層次 (table-level) 和記錄層次 (record-level) 的操作分開。
使用 Manager
的 all()
方法 來取出所有物件:
>>> all_entries = Entry.objects.all()
使用 all()
回傳的是整個集合,使用過濾器回傳的則是子集。
為了建立子集,透過加入過濾器條件,來縮小 (refine) 初始的 QuerySet
。
最常用來重定義查詢集的方法有兩種:
回傳新的查詢集,集合中的物件符合給定的查詢參數。
回傳不符合查詢參數的的查詢集。
查詢參數 **kwargs
的格式需符合 Field lookups
裡所指。
取得 2006 年部落格文章的查詢集:
Entry.objects.filter(pub_date__year=2006)
等價於
Entry.objects.all().filter(pub_date__year=2006)
縮小查詢集的結果也是查詢集,所以可以將細化的過程接起來:
>>> Entry.objects.filter(
... headline__startswith='What'
... ).exclude(
... pub_date__gte=datetime.date.today()
... ).filter(
... pub_date__gte=datetime(2005, 1, 30)
... )
最終結果的查詢集包含所有 headline
起頭是 'What',發佈日期介於 2005/01/30 至今天的文章。
每次對一個查詢集做細化所得的新查詢集都是新的,沒有跟舊的綁在一起。
>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())
q2
和 q3
是延伸自 q1
,但 q1
不受後者影響。
建立查詢集的動作並不包含任何對資料庫的操作,不管如何堆疊過濾器也一樣。直到查詢集被計算 (evaluated),才會對資料庫做存取。
>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)
前三行的過濾器操作其實完全未對資料庫進行任何操作,直到 print(q)
。一般來說,直到你真正要取值,查詢集是不會對資料庫做查詢的。
參考:When QuerySets are evaluated
像 filter()
或 exclude()
等等的過瀘器總是回傳 QuerySet。即便只有一個物件符合查詢結果,仍會回傳只有該物件的集合。
若確認只有唯一一個物件符合查詢,可以使用 Manager
的 get()
方法,直接回傳查詢物件。
>>> one_entry = Entry.objects.get(pk=1)
任何用在過濾器上的查詢表示式都可以套用在 get()
上,語法一樣參考 Field lookups。
get()
和 filter()
在使用上的一個不同是其回傳值。若沒有符合的查詢結果,get()
會回傳 DoesNotExist
的例外,相反的,若用 get()
查詢的結果超過一筆,也會回傳例外 MultipleObjectsReturned
。
DoesNotExist
和 MultipleObjectsReturned
都是資料模型類別的屬性,以上個例子來講,就是Entry.DoesNotExist
和 Entry.MultipleObjectsReturned
。
除了 all()
、filter()
和 exclude()
外,還有許多查詢方法,完整的 QuerySet
方法參考 QuerySet API Reference。
使用 Python 的部分 array-slicing 語法來限制 QuerySet 的輸出結果數量。這等價於 SQL 的 LIMIT
和 OFFSET
語法。
回傳前 5 筆查詢結果 (LIMIT 5
):
>>> Entry.objects.all()[:5]
回傳從第 6 到第 10 個物件 (OFFSET 5 LIMIT 5
)
>>> Entry.objects.all()[5:10]
但像是回傳最後一個物件用的 negative indexing 就不支援
>>> Entry.objects.all()[-1]
一般來說,切割 QuerySet 回傳的是新 QuerySet,一樣是真正要取值時才計算。一個例外是使用步進參數 (step parameter):
>>> Entry.objects.all()[:10:2]
這種情況下,會確實執行查詢,回傳前 10 筆以 2 為步進的清單。
要取得單一物件,而不是整個清單的話 (如 SELECT foo FROM bar LIMIT 1
),直接使用 array index 來取代 slice:
>>> type(Entry.objects.order_by('headline'))
django.db.models.query.QuerySet
>>> e1 = Entry.objects.order_by('headline')[0]
這大致上等同於:
>>> e2 = Entry.objects.order_by('headline')[0:1].get()
e1
是在回傳的 QuerySet 上使用 array index,e2
是在 QuerySet 上使用 index slicing 縮小到只有一個元素的集合再用 get()
(注意前面有提到,對 QuerySet 做 slicing 後仍是 QuerySet,可以使用 get()
)。
也因如此,二者在遇到沒有符合的查詢結果時回傳的例外不一樣,前者回傳 IndexError
,後者回傳 DoesNotExist
。
In [53]: e = Entry.objects.filter(headline__startswith='abc')
In [55]: e
Out[55]: []
In [56]: e[0]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-56-3c9af7fabe24> in <module>()
...
...
...
IndexError: list index out of range
對前者來說,Entry.objects.filter(headline__startswith='abc')
本來就是空集合 QuerySet,再用 array index 只是從一個沒有內容物的集合中取物,所以得到 IndexError
這個例外。
而後者,一個空的 QuerySet 再經 slicing,仍舊是 QuerySet,雖然可以使用 get()
方法,但因沒有物件,所以回傳 DoesNotExist
這個例外。
欄位查詢對應到 SQL WHERE
語法。它們以關鍵字參數 (keyword argument) 指派到 QuerySet 方法中。
基本的查詢關鍵字參數使用 field__lookuptype=value
的格式,也就是欄位名稱透過雙底線 __
接查詢種類。
>>> Entry.objects.filter(pub_date__lte='2006-01-01')
大概等價於
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01'
能辦到這一點是因為 Python 能定義接受任意 key-value 參數的函式,並在 runtime 期間對這些參數進行評估。參考官方文件 Keyword Arguments。
查詢中用的欄位必須是 model field 的名稱。但使用外鍵時可以在欄位名稱後接 _id
。
In [74]: b = Blog.objects.all()[0]
In [75]: b
Out[75]: <Blog: Beatles Blog>
In [76]: Entry.objects.filter(blog=b)
Out[76]: [<Entry: Beatles is dead.>]
In [77]: Entry.objects.filter(blog_id=b.id)
Out[77]: [<Entry: Beatles is dead.>]
注意,在使用後綴 _id
的情況下,參數值得是關聯 model 的主鍵值:
>>> Entry.objects.filter(blog_id=4)
若傳不合的 keyword argument,會回傳 TypeError
。
整個資料庫 API 提供的查詢類型大約超過二打,完整的清單參考 field lookup reference。
以下列出較常使用的查詢…
回傳精確符合的結果
>>> Entry.objects.get(headline__exact="Man bites dog")
對應的 SQL 語法是
SELECT ... WHERE headline = 'Man bites dog';
當只提供欄位,而沒有再用雙底線後接查詢類型,預設使用 exact
lookup type。
舉例來說,以下兩個查詢是等價的:
>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
大小寫不分 (case-insensitive) 的精確符合查詢
>>> Blog.objects.get(name__iexact="beatles blog")
大小寫有分 (case-sensitive) 的「包含」查詢:
Entry.objects.get(headline__contains='Lennon')
大約等價於以下的 SQL 語法:
SELECT ... WHERE headline LIKE '%Lennon%';
同樣的,也有不分大小寫的版本 icontains
。
完整的查詢清單:field lookup reference
在此列出 1.6 版的 field lookups 完整清單,共 25 個:
- exact
- iexact
- contains
- icontains
- in
- gt
- gte
- lt
- lte
- startswith
- istartswith
- endswith
- iendswith
- range
- year
- month
- day
- week_day
- hour
- minute
- second
- isnull
- search
- regex
- iregex
Django 也提供欄位後加雙底線的方式延展關聯,自動實現 SQL JOINs
的行為。
下例說明如何查詢所有外鍵關聯 Blog
的 name
為 'Beatles Blog' 的 Entry
物件。
>>> Entry.objects.filter(blog__name='Beatles Blog')
同樣的,也能反查 (reverse relationship):
Blog.objects.filter(entries__headline__contains='Beatles')
當查詢跨越多層關聯,而其中有的 model 值沒有符合過濾條件,Django 會將其視為一個空 (卻合法,所有值都為 NULL
) 的物件。沒有例外錯誤會出現。
Blog.objects.filter(entry__authors__name='Lennon')
上面的例子中,若一個 Entry
沒有任何 author
與其有關聯,會被視為也沒有 name
附加在其上。以此取代發出沒有 author
的例外。
但有一種查詢的使用會讓人混淆
Blog.objects.filter(entries__authors__name__isnull=True)
這樣的查詢不只回傳對 author
是空白的 name
,也回傳對 entry
是空白的 author
的 Blog
物件。如果不想要後者,可以改寫成
Blog.objects.filter(entry__authors__isnull=False,
entry__authors__name__isnull=True)
在對多對多欄位或反向外鍵做查詢時,有兩種不同的過濾器可以用。
考慮 Blog/Entry
關聯 (Blog
對 Entry
是一對多)。我們可能想找出哪些 Blog
,其 Entry
的 headline
為 "Lennon", 且 (and)在 2008 年發表。或是想找出有哪些 Blog
,它的 Entry
headline
為 "Lennon" 或(or) 是在 2008 年發表的。
同時的情況跟需求也出現在多對多欄位。舉例,若一個 Entry
有個多對多欄位 tags
,我們可能會想找出有哪些 entry 有 'music' 跟 'bands' 這兩個 tag。又或許我們想找出哪些 entry 有 tag 'music',且 status
欄位為 public
。
簡單來說,一個是且(and),另一個是或(or) 的情況。
解 且 的做法是把所有條件都塞在一個 filter
裡:
Blog.objects.filter(entry__headline__contains='Lennon',
entry__pub_date__year=2008)
解 或 的做法是串接 filter
。串接 filter
的用法在做關聯查詢時的意義跟之前提到的不同。原本的串接用法,會進一步縮小查詢集。但若 filter 內的 lookup 是針對關聯,作用的是任何跟主 model 有關的,而不是針對先前一個 filter 輸出的 QuerySet 做查詢。
Blog.objects.filter(entry__headline__contains='Lennon').filter(
entry__pub_date__year=2008)