Django4.0 執(zhí)行查詢-檢索對象

2022-03-16 17:33 更新

要從數(shù)據(jù)庫檢索對象,要通過模型類的 ?Manager ?構(gòu)建一個 ?QuerySet?。

一個 QuerySet 代表來自數(shù)據(jù)庫中對象的一個集合。它可以有0個,1個或者多個 ?filters. Filters?,可以根據(jù)給定參數(shù)縮小查詢結(jié)果量。在 SQL 的層面上, ?QuerySet ?對應 ?SELECT ?語句,而?*filters*?對應類似 ?WHERE ?或 ?LIMIT ?的限制子句。

你能通過模型的 ?Manager ?獲取 ?QuerySet?。每個模型至少有一個 ?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 只能通過模型類訪問,而不是通過模型實例,目的是強制分離 “表級” 操作和 “行級” 操作。

?Manager ?是模型的 ?QuerySets ?主要來源。例如 ?Blog.objects.all()? 返回了一個 ?QuerySet?,后者包含了數(shù)據(jù)庫中所有的 ?Blog ?對象。

檢索全部對象

從數(shù)據(jù)庫中檢索對象最簡單的方式就是檢索全部。為此,在 ?Manager ?上調(diào)用 ?all()? 方法:

>>> all_entries = Entry.objects.all()

方法 ?all()? 返回了一個包含數(shù)據(jù)庫中所有對象的 ?QuerySet ?對象。

通過過濾器檢索指定對象

?all() ?返回的 ?QuerySet ?包含了數(shù)據(jù)表中所有的對象。雖然,大多數(shù)情況下,你只需要完整對象集合的一個子集。要創(chuàng)建一個這樣的子集,你需要通過添加過濾條件精煉原始 ?QuerySet?。兩種最常見的精煉 ?QuerySet ?的方式是:

  • ?filter(**kwargs)?返回一個新的 ?QuerySet?,包含的對象滿足給定查詢參數(shù)。
  • ?exclude(**kwargs)?返回一個新的 ?QuerySet?,包含的對象不滿足給定查詢參數(shù)。

例如,要包含獲取 2006 年的博客條目(entries blog)的 ?QuerySet?,像這樣使用 ?filter()?:

Entry.objects.filter(pub_date__year=2006)

通過默認管理器類也一樣:

Entry.objects.all().filter(pub_date__year=2006)

鏈式過濾器

精煉 ?QuerySet ?的結(jié)果本身還是一個 ?QuerySet?,所以能串聯(lián)精煉過程。例子:

>>> Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.date.today()
... ).filter(
...     pub_date__gte=datetime.date(2005, 1, 30)
... )

這個先獲取包含數(shù)據(jù)庫所有條目(?entry?)的 ?QuerySet?,然后排除一些,再進入另一個過濾器。最終的 ?QuerySet ?包含標題以 ?"What"? 開頭的,發(fā)布日期介于 2005 年 1 月 30 日與今天之間的所有條目。

每個 QuerySet 都是唯一的

每次精煉一個 ?QuerySet?,你就會獲得一個全新的 ?QuerySet?,后者與前者毫無關(guān)聯(lián)。每次精煉都會創(chuàng)建一個單獨的、不同的 ?QuerySet?,能被存儲,使用和復用。
舉例:

>>> 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())

這三個 ?QuerySets ?是獨立的。第一個是基礎(chǔ) ?QuerySet?,包含了所有標題以 ?"What"? 開頭的條目。第二個是第一個的子集,帶有額外條件,排除了 ?pub_date ?是今天和今天之后的所有記錄。第三個是第一個的子集,帶有額外條件,只篩選 ?pub_date ?是今天或未來的所有記錄。最初的 ?QuerySet (q1)? 不受篩選操作影響。

QuerySet 是惰性的

?QuerySet ?是惰性的 —— 創(chuàng)建 ?QuerySet ?并不會引發(fā)任何數(shù)據(jù)庫活動。你可以將一整天的過濾器都堆積在一起,Django 只會在 ?QuerySet ?被計算時執(zhí)行查詢操作。來瞄一眼這個例子:

>>> 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)

雖然這看起來像是三次數(shù)據(jù)庫操作,實際上只在最后一行 ?(print(q))? 做了一次。一般來說, ?QuerySet ?的結(jié)果直到你 要使用時才會從數(shù)據(jù)庫中拿出。當你要用時,才通過數(shù)據(jù)庫計算出 ?QuerySet?。

用 get() 檢索單個對象

?filter()? 總是返回一個 ?QuerySet?,即便只有一個對象滿足查詢條件 —— 這種情況下, ?QuerySet ?只包含了一個元素。
若你知道只會有一個對象滿足查詢條件,你可以在 ?Manager ?上使用 ?get()? 方法,它會直接返回這個對象:

>>> one_entry = Entry.objects.get(pk=1)

你可以對 ?get()? 使用與 ?filter() ?類似的所有查詢表達式。
注意, 使用切片 [0] 時的 ?get()? 和 ?filter()? 有點不同。如果沒有滿足查詢條件的結(jié)果, ?get()? 會拋出一個 ?DoesNotExist ?異常。該異常是執(zhí)行查詢的模型類的一個屬性 —— 所有,上述代碼中,若沒有哪個 ?Entry ?對象的主鍵是 1,Django 會拋出 ?Entry.DoesNotExist?。
類似了,Django 會在有不止一個記錄滿足 ?get() ?查詢條件時發(fā)出警告。這時,Django 會拋出 ?MultipleObjectsReturned?,這同樣也是模型類的一個屬性。

其它 QuerySet 方法

大多數(shù)情況下,你會在需要從數(shù)據(jù)庫中檢索對象時使用 ?all()?, ?get()?, ?filter()? 和 ?exclude()?。

限制 QuerySet 條目數(shù)

利用 Python 的數(shù)組切片語法將 ?QuerySet ?切成指定長度。這等價于 SQL 的 ?LIMIT ?和 ?OFFSET ?子句。
例如,這將返回前 5 個對象 (?LIMIT 5?):

>>> Entry.objects.all()[:5]

這會返回第 6 至第 10 個對象 (?OFFSET 5 LIMIT 5?):

>>> Entry.objects.all()[5:10]

不支持負索引 (例如 ?Entry.objects.all()[-1]?)
一般情況下, ?QuerySet ?的切片返回一個新的 ?QuerySet ?—— 其并未執(zhí)行查詢。一個特殊情況是使用了的 Python 切片語法的步長。例如,這將會實際的執(zhí)行查詢命令,為了獲取從前 10 個對象中,每隔一個抽取的對象組成的列表:

>>> Entry.objects.all()[:10:2]

由于對 ?queryset ?切片工作方式的模糊性,禁止對其進行進一步的排序或過濾。
要檢索 單個 對象而不是一個列表時(例如 ?SELECT foo FROM bar LIMIT 1?),請使用索引,而不是切片。例如,這會返回按標題字母排序后的第一個 ?Entry?:

>>> Entry.objects.order_by('headline')[0]

這大致等價于:

>>> Entry.objects.order_by('headline')[0:1].get()

然而,注意一下,若沒有對象滿足給定條件,前者會拋出 ?IndexError?,而后者會拋出 ?DoesNotExist?。

字段查詢

字段查詢即你如何制定 SQL ?WHERE ?子句。它們以關(guān)鍵字參數(shù)的形式傳遞給 ?QuerySet ?方法? filter()?, ?exclude()? 和 ?get()?。
基本的查詢關(guān)鍵字參數(shù)遵照 ?field__lookuptype=value?。(有個雙下劃線)。例如:

>>> Entry.objects.filter(pub_date__lte='2006-01-01')

轉(zhuǎn)換為 SQL 語句大致如下:

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

查詢子句中指定的字段必須是模型的一個字段名。不過也有個例外,在 ?ForeignKey ?中,你可以指定以 ?_id? 為后綴的字段名。這種情況下,?value參數(shù)需要包含 ?foreign ?模型的主鍵的原始值。例子:

>>> Entry.objects.filter(blog_id=4)

若你傳入了無效的關(guān)鍵字參數(shù),查詢函數(shù)會拋出 ?TypeError?。
數(shù)據(jù)庫 API 支持兩套查詢類型。為了讓你了解能干啥,以下是一些常見的查詢:

exact

一個 exact? 匹配的例子:

>>> Entry.objects.get(headline__exact="Cat bites dog")

會生成這些 SQL:

SELECT ... WHERE headline = 'Cat bites dog';

若你為提供查詢類型 —— 也就說,若關(guān)鍵字參數(shù)未包含雙下劃線 —— 查詢類型會被指定為 ?exact?。
例如,以下兩條語句是等價的:

>>> Blog.objects.get(id__exact=14)  # Explicit form
>>> Blog.objects.get(id=14)         # __exact is implied

這是為了方便,因為 ?exact ?查詢是最常見的。

iexact

不分大小寫的匹配,查詢語句:

>>> Blog.objects.get(name__iexact="beatles blog")

會匹配標題為 ?"Beatles Blog"?, ?"beatles blog"?, 甚至 ?"BeAtlES blOG"? 的 ?Blog?。

contains

大小寫敏感的包含測試。例子:

Entry.objects.get(headline__contains='Lennon')

粗略地轉(zhuǎn)為 SQL:

SELECT ... WHERE headline LIKE '%Lennon%';

注意這將匹配標題 '?Today Lennon honored'?,而不是 ?'today lennon honored'?。
這也有個大小寫不敏感的版本, ?icontains?。
?startswith?, ?endswith?

分別以搜索開始和以搜索結(jié)束。

還有不區(qū)分大小寫的版本,稱為 ?isstartswith ?和 ?iendswith?。 

跨關(guān)系查詢

Django 提供了一種強大而直觀的方式來追蹤查詢中的關(guān)系,在幕后自動為你處理 SQL ?JOIN ?關(guān)系。為了跨越關(guān)系,跨模型使用關(guān)聯(lián)字段名,字段名由雙下劃線分割,直到拿到想要的字段。
本例檢索出所有的 ?Entry ?對象,其 ?Blog ?的 ?name ?為 ?'Beatles Blog'? :

>>> Entry.objects.filter(blog__name='Beatles Blog')

跨域的深度隨你所想。
它也可以反向工作。雖然它可以自定義,默認情況下,你在查找中使用模型的小寫名稱來引用一個反向關(guān)系。
本例檢索的所有 ?Blog ?對象均擁有至少一個 標題 含有 ?'Lennon'? 的條目:

>>> Blog.objects.filter(entry__headline__contains='Lennon')

如果你在跨多個關(guān)系進行篩選,而某個中間模型的沒有滿足篩選條件的值,Django 會將它當做一個空的(所有值都是 ?NULL?)但是有效的對象。這樣就意味著不會拋出錯誤。例如,在這個過濾器中:

Blog.objects.filter(entry__authors__name='Lennon')

(假設(shè)有個關(guān)聯(lián)的 ?Author ?模型),若某項條目沒有任何關(guān)聯(lián)的 ?author?,它會被視作沒有關(guān)聯(lián)的 ?name?,而不是因為缺失 ?author ?而拋出錯誤。大多數(shù)情況下,這就是你期望的。唯一可能使你迷惑的場景是在使用 ?isnull ?時。因此:

Blog.objects.filter(entry__authors__name__isnull=True)

將會返回 ?Blog ?對象,包含 ?author ?的 ?name ?為空的對象,以及那些 ?entry ?的 ?author ?為空的對象。若你不想要后面的對象,你可以這樣寫:

Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)

跨多值關(guān)聯(lián)

當跨越 ?ManyToManyField ?或反查 ?ForeignKey ?(例如從 ?Blog ?到 ?Entry ?)時,對多個屬性進行過濾會產(chǎn)生這樣的問題:是否要求每個屬性都在同一個相關(guān)對象中重合。我們可能會尋找那些在標題中含有 ?“Lennon”? 的 2008 年的博客,或者我們可能會尋找那些僅有 2008 年的任何條目以及一些在標題中含有 ?“Lennon”? 的較新或較早的條目。
要選擇所有包含 2008 年至少一個標題中有? "Lennon"? 的條目的博客(滿足兩個條件的同一條目),我們要寫:

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

否則,如果要執(zhí)行一個更為寬松的查詢,選擇任何只在標題中帶有 ?"Lennon"? 的條目和 2008 年的條目的博客,我們將寫:

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

假設(shè)只有一個博客既有包含 ?"Lennon"? 的條目又有 2008 年的條目,但 2008 年的條目中沒有包含 ?"Lennon"? 。第一個查詢不會返回任何博客,但第二個查詢會返回那一個博客。(這是因為第二個過濾器選擇的條目可能與第一個過濾器中的條目相同,也可能不相同)。我們是用每個過濾器語句來過濾 ?Blog ?項,而不是 ?Entry ?項)。簡而言之,如果每個條件需要匹配相同的相關(guān)對象,那么每個條件應該包含在一個 ?filter()? 調(diào)用中。

由于第二個查詢鏈接了多個過濾器,它對主模型進行了多次連接,可能會產(chǎn)生重復的結(jié)果。

>>> from datetime import date
>>> beatles = Blog.objects.create(name='Beatles Blog')
>>> pop = Blog.objects.create(name='Pop Music Blog')
>>> Entry.objects.create(
...     blog=beatles,
...     headline='New Lennon Biography',
...     pub_date=date(2008, 6, 1),
... )
<Entry: New Lennon Biography>
>>> Entry.objects.create(
...     blog=beatles,
...     headline='New Lennon Biography in Paperback',
...     pub_date=date(2009, 6, 1),
... )
<Entry: New Lennon Biography in Paperback>
>>> Entry.objects.create(
...     blog=pop,
...     headline='Best Albums of 2008',
...     pub_date=date(2008, 12, 15),
... )
<Entry: Best Albums of 2008>
>>> Entry.objects.create(
...     blog=pop,
...     headline='Lennon Would Have Loved Hip Hop',
...     pub_date=date(2020, 4, 1),
... )
<Entry: Lennon Would Have Loved Hip Hop>
>>> Blog.objects.filter(
...     entry__headline__contains='Lennon',
...     entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>]>
>>> Blog.objects.filter(
...     entry__headline__contains='Lennon',
... ).filter(
...     entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>, <Blog: Beatles Blog>, <Blog: Pop Music Blog]>

注解:

?filter()? 的查詢行為會跨越多值關(guān)聯(lián),就像前文說的那樣,并不與 ?exclude()? 相同。相反,一次 ?exclude()? 調(diào)用的條件并不需要指向同一項目。
例如,以下查詢會排除那些關(guān)聯(lián)條目標題包含 ?"Lennon"? 且發(fā)布于 2008 年的博客:

Blog.objects.exclude(
    entry__headline__contains='Lennon',
    entry__pub_date__year=2008,
)

但是,與? filter()? 的行為不同,其并不會限制博客同時滿足這兩種條件。要這么做的話,也就是篩選出所有條目標題不帶 ?"Lennon" ?且發(fā)布年不是 2008 的博客,你需要做兩次查詢:

Blog.objects.exclude(
    entry__in=Entry.objects.filter(
        headline__contains='Lennon',
        pub_date__year=2008,
    ),
)

過濾器可以為模型指定字段

在之前的例子中,我們已經(jīng)構(gòu)建過的 ?filter ?都是將模型字段值與常量做比較。但是,要怎么做才能將模型字段值與同一模型中的另一字段做比較呢?
Django 提供了 ?F? 表達式 實現(xiàn)這種比較。 ?F()? 的實例充當查詢中的模型字段的引用。這些引用可在查詢過濾器中用于在同一模型實例中比較兩個不同的字段。
例如,要查出所有評論數(shù)大于 ?pingbacks ?的博客條目,我們構(gòu)建了一個 ?F() ?對象,指代 ?pingback ?的數(shù)量,然后在查詢中使用該 ?F() ?對象:

>>> from django.db.models import F
>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks'))

Django 支持對 ?F()? 對象進行加、減、乘、除、求余和次方,另一操作數(shù)既可以是常量,也可以是其它 F() 對象。要找到那些評論數(shù)兩倍于 ?pingbacks的博客條目,我們這樣修改查詢條件:

>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks') * 2)

要找出所有評分低于 pingback 和評論總數(shù)之和的條目,修改查詢條件:

>>> Entry.objects.filter(rating__lt=F('number_of_comments') + F('number_of_pingbacks'))

你也能用雙下劃線在 ?F()? 對象中通過關(guān)聯(lián)關(guān)系查詢。帶有雙下劃線的 ?F()? 對象將引入訪問關(guān)聯(lián)對象所需的任何連接。例如,要檢索出所有作者名與博客名相同的博客,這樣修改查詢條件:

>>> Entry.objects.filter(authors__name=F('blog__name'))

對于 ?date ?和 ?date/time? 字段,你可以加上或減去一個 ?timedelta ?對象。以下會返回所有發(fā)布 3 天后被修改的條目:

>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

?F()? 對象通過 ?.bitand()?, ?.bitor()?, ?.bitxor()?,?.bitrightshift()? 和? .bitleftshift()? 支持位操作。例如:

>>> F('somefield').bitand(16)

表達式可以引用轉(zhuǎn)換

Django 支持在表達式中使用轉(zhuǎn)換。

例如,要查找與上次修改同一年發(fā)布的所有條目對象:

>>> Entry.objects.filter(pub_date__year=F('mod_date__year'))

要查找條目發(fā)布的最早年份,我們可以發(fā)出查詢:

>>> Entry.objects.aggregate(first_published_year=Min('pub_date__year'))

此示例查找最高評分條目的值以及每年所有條目的評論總數(shù):

>>> Entry.objects.values('pub_date__year').annotate(
...     top_rating=Subquery(
...         Entry.objects.filter(
...             pub_date__year=OuterRef('pub_date__year'),
...         ).order_by('-rating').values('rating')[:1]
...     ),
...     total_comments=Sum('number_of_comments'),
... )

主鍵(pk)查詢快捷方式

出于方便的目的,Django 提供了一種 ?pk ?查詢快捷方式, ?pk ?表示主鍵 ?"primary key"?。
示例 ?Blog ?模型中,主鍵是 ?id ?字段,所以這 3 個語句是等效的:

>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

?pk ?的使用并不僅限于? __exact? 查詢——任何的查詢項都能接在 ?pk ?后面,執(zhí)行對模型主鍵的查詢:

# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])

# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)

?pk ?查找也支持跨連接。例如,以下 3 個語句是等效的:

>>> Entry.objects.filter(blog__id__exact=3) # Explicit form
>>> Entry.objects.filter(blog__id=3)        # __exact is implied
>>> Entry.objects.filter(blog__pk=3)        # __pk implies __id__exact

在LIKE語句中轉(zhuǎn)義百分號和下劃線

等效于 ?LIKE ?SQL 語句的字段查詢子句 (?iexact?, ?contains?, ?icontains?, ?startswith?, ?istartswith?, ?endswith ?和 ?iendswith?) 會將 ?LIKE ?語句中有特殊用途的兩個符號,即百分號和下劃線自動轉(zhuǎn)義。(在 ?LIKE ?語句中,百分號匹配多個任意字符,而下劃線匹配一個任意字符。)
例如,要檢索所有包含百分號的條目:

>>> Entry.objects.filter(headline__contains='%')

Django 為你處理了引號;生成的 SQL 語句看起來像這樣:

SELECT ... WHERE headline LIKE '%\%%';

同樣的處理也包括下劃線。百分號和下劃線都為你自動處理,你無需擔心。

緩存和QuerySet

每個 ?QuerySet ?都帶有緩存,盡量減少數(shù)據(jù)庫訪問。理解它是如何工作的能讓你編寫更高效的代碼。
新創(chuàng)建的 ?QuerySet ?緩存是空的。一旦要計算 ?QuerySet ?的值,就會執(zhí)行數(shù)據(jù)查詢,隨后,Django 就會將查詢結(jié)果保存在 ?QuerySet ?的緩存中,并返回這些顯式請求的緩存(例如,下一個元素,若 ?QuerySet ?正在被迭代)。后續(xù)針對 ?QuerySet ?的計算會復用緩存結(jié)果。
牢記這種緩存行為,在你錯誤使用 ?QuerySet ?時可能會被它咬一下。例如,以下會創(chuàng)建兩個 ?QuerySet?,計算它們,丟掉它們:

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

這意味著同樣的數(shù)據(jù)庫查詢會被執(zhí)行兩次,實際加倍了數(shù)據(jù)庫負載。同時,有可能這兩個列表不包含同樣的記錄,因為在兩次請求間,可能有 ?Entry ?被添加或刪除了。
要避免此問題,保存 ?QuerySet ?并復用它:

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation.

當 QuerySet 未被緩存時

查詢結(jié)果集并不總是緩存結(jié)果。當僅計算查詢結(jié)果集的 部分 時,會校驗緩存,若沒有填充緩存,則后續(xù)查詢返回的項目不會被緩存。特別地說,這意味著使用數(shù)組切片或索引的 限制查詢結(jié)果集 不會填充緩存。
例如,重復的從某個查詢結(jié)果集對象中取指定索引的對象會每次都查詢數(shù)據(jù)庫:

>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # Queries the database
>>> print(queryset[5]) # Queries the database again

不過,若全部查詢結(jié)果集已被檢出,就會去檢查緩存:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print(queryset[5]) # Uses cache
>>> print(queryset[5]) # Uses cache

以下展示一些例子,這些動作會觸發(fā)計算全部的查詢結(jié)果集,并填充緩存的過程:

>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

注意:只是打印查詢結(jié)果集不會填充緩存。因為調(diào)用 ?__repr__() ?僅返回了完整結(jié)果集的一個切片。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號