建立查詢

一旦您建立了資料模型,Django 就會自動為您提供一個資料庫抽象 API,讓您能夠建立、擷取、更新和刪除物件。本文說明如何使用此 API。請參閱資料模型參考,以取得所有各種模型查找選項的完整詳細資訊。

在本指南 (以及參考資料) 中,我們將參考以下模型,這些模型組成一個部落格應用程式

from datetime import date

from django.db import models


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self):
        return self.name


class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField(default=date.today)
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField(default=0)
    number_of_pingbacks = models.IntegerField(default=0)
    rating = models.IntegerField(default=5)

    def __str__(self):
        return self.headline

建立物件

為了在 Python 物件中表示資料庫表格資料,Django 使用了一個直觀的系統:模型類別表示資料庫表格,而該類別的實例表示資料庫表格中的特定記錄。

若要建立物件,請使用關鍵字引數來實例化模型類別,然後呼叫save()將其儲存到資料庫。

假設模型位於 Django 應用程式blog內的models.py檔案中,以下為範例

>>> from blog.models import Blog
>>> b = Blog(name="Beatles Blog", tagline="All the latest Beatles news.")
>>> b.save()

這會在幕後執行INSERT SQL 語句。在您明確呼叫save()之前,Django 不會存取資料庫。

save()方法沒有傳回值。

另請參閱

save()接受許多此處未說明的進階選項。請參閱save()的文件以取得完整詳細資訊。

若要在單一步驟中建立並儲存物件,請使用create()方法。

儲存對物件的變更

若要儲存對已在資料庫中的物件所做的變更,請使用save()

假設有一個已經儲存到資料庫的Blog實例b5,此範例會變更其名稱並更新資料庫中的記錄

>>> b5.name = "New name"
>>> b5.save()

這會在幕後執行UPDATE SQL 語句。在您明確呼叫save()之前,Django 不會存取資料庫。

儲存ForeignKeyManyToManyField欄位

更新ForeignKey欄位的運作方式與儲存一般欄位完全相同 – 將正確類型的物件指派給相關欄位。此範例更新Entry實例entryblog屬性,假設EntryBlog的適當實例已儲存到資料庫 (因此我們可以在下面擷取它們)

>>> from blog.models import Blog, Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

更新ManyToManyField的運作方式略有不同 – 使用欄位上的add()方法將記錄新增到關聯。此範例將Author實例joe新增至entry物件

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

若要一次將多個記錄新增至ManyToManyField,請在呼叫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)

如果您嘗試指派或新增錯誤類型的物件,Django 會發出抱怨。

擷取物件

若要從資料庫擷取物件,請透過模型類別上的Manager建構QuerySet

QuerySet表示資料庫中的物件集合。它可以有零個、一個或多個篩選器。篩選器會根據給定的參數縮小查詢結果。以 SQL 術語來說,QuerySet等同於SELECT語句,而篩選器是諸如WHERELIMIT之類的限制子句。

您可以透過使用模型的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."

注意

為了強制分隔「表格層級」操作和「記錄層級」操作,Manager只能透過模型類別存取,而不能透過模型實例存取。

Manager是模型的QuerySets的主要來源。例如,Blog.objects.all()會傳回一個QuerySet,其中包含資料庫中的所有Blog物件。

擷取所有物件

從表格中擷取物件的最簡單方法是擷取所有物件。若要執行此動作,請在Manager上使用all()方法

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

all()方法會傳回資料庫中所有物件的QuerySet

使用篩選器擷取特定物件

QuerySetall()傳回的描述資料庫表格中的所有物件。不過,通常您只需要選取完整物件集合的子集。

若要建立這樣的子集,請完善初始的QuerySet,新增篩選條件。完善QuerySet的兩種最常見方式為

filter(**kwargs)

傳回新的QuerySet,其中包含符合給定查找參數的物件。

exclude(**kwargs)

傳回新的QuerySet,其中包含符合給定查找參數的物件。

查詢參數(上述函式定義中的 **kwargs)應採用下方欄位查詢中所述的格式。

例如,若要取得 2006 年的部落格文章 QuerySet,可以使用 filter(),如下所示:

Entry.objects.filter(pub_date__year=2006)

使用預設的管理器類別,效果等同於:

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

鏈式篩選

精煉 QuerySet 的結果本身也是一個 QuerySet,因此可以將精煉操作串聯起來。例如:

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

這會先取得資料庫中所有文章的初始 QuerySet,然後加入篩選條件,接著加入排除條件,最後再加入另一個篩選條件。最終結果是一個 QuerySet,其中包含標題以「What」開頭,且發佈日期介於 2005 年 1 月 30 日到當天之間的所有文章。

篩選過的 QuerySet 是獨一無二的

每次精煉 QuerySet 時,都會得到一個全新的 QuerySet,它與先前的 QuerySet 沒有任何關聯。每次精煉都會建立一個獨立且不同的 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())

這三個 QuerySet 是獨立的。第一個是基本的 QuerySet,其中包含所有標題以「What」開頭的文章。第二個是第一個的子集,並額外加上排除 pub_date 為今天或未來日期的記錄的條件。第三個是第一個的子集,並額外加上只選擇 pub_date 為今天或未來日期的記錄的條件。最初的 QuerySetq1)不受精煉過程的影響。

QuerySet 是惰性的

QuerySet 是惰性的 - 建立 QuerySet 的動作不會涉及任何資料庫活動。您可以不斷堆疊篩選條件,而 Django 實際上不會執行查詢,直到 QuerySet評估為止。請看以下範例:

>>> 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))命中資料庫一次。一般而言,QuerySet 的結果在您「要求」它們之前,不會從資料庫中擷取。當您要求時,QuerySet 會透過存取資料庫來進行評估。如需評估發生的確切時機的更多詳細資訊,請參閱何時評估 QuerySet

使用 get() 擷取單一物件

filter() 總是會提供一個 QuerySet,即使只有一個物件符合查詢條件 - 在這種情況下,它會是一個包含單一元素的 QuerySet

如果您知道只有一個物件符合您的查詢條件,您可以使用 get() 方法在 Manager 上,此方法會直接傳回物件。

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

您可以對 get() 使用任何查詢表達式,就像對 filter() 一樣 - 再次參閱下方的欄位查詢

請注意,使用 get() 和使用 filter() 並搭配切片 [0] 之間存在差異。如果沒有符合查詢的結果,get() 會引發 DoesNotExist 例外。此例外是正在執行查詢的模型類別的一個屬性 - 因此在上面的程式碼中,如果沒有主鍵為 1 的 Entry 物件,Django 會引發 Entry.DoesNotExist

同樣地,如果有多個項目符合 get() 查詢,Django 也會提出警告。在這種情況下,它會引發 MultipleObjectsReturned,這也是模型類別本身的一個屬性。

其他 QuerySet 方法

大多數時候,當您需要從資料庫查詢物件時,會使用 all()get()filter()exclude()。然而,這遠非全部;請參閱QuerySet API 參考,以取得所有各種 QuerySet 方法的完整清單。

限制 QuerySet

使用 Python 陣列切片語法的一個子集,將您的 QuerySet 限制為特定數量的結果。這相當於 SQL 的 LIMITOFFSET 子句。

例如,這會傳回前 5 個物件(LIMIT 5

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

這會傳回第 6 到第 10 個物件(OFFSET 5 LIMIT 5

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

不支援負索引(例如 Entry.objects.all()[-1])。

一般而言,切片 QuerySet 會傳回一個新的 QuerySet – 它不會評估查詢。例外情況是您使用 Python 切片語法的「step」參數。例如,這實際上會執行查詢,以傳回前 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。有關詳細資訊,請參閱 get()

欄位查詢

欄位查詢是您如何指定 SQL WHERE 子句的核心內容。它們被指定為 QuerySet 方法 filter()exclude()get() 的關鍵字引數。

基本查詢關鍵字引數採用 field__lookuptype=value 的形式。(這是一個雙底線)。例如

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

(大致)翻譯成以下 SQL

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

這如何運作

Python 具有定義函式的功能,該函式接受任意名稱-值引數,這些引數的名稱和值會在執行時計算。有關更多資訊,請參閱官方 Python 教學中的 關鍵字引數

查詢中指定的欄位必須是模型欄位的名稱。但是,在 ForeignKey 的情況下,您可以指定以 _id 為後綴的欄位名稱。在這種情況下,值參數預期包含外來模型的主鍵的原始值。例如

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

如果您傳遞無效的關鍵字引數,查詢函式會引發 TypeError

資料庫 API 支援大約二十多種查詢類型;完整的參考可以在欄位查詢參考中找到。為了讓您了解可用的內容,以下是一些您可能會更常用到的查詢:

exact

「完全」符合。例如

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

會產生類似以下的 SQL

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

如果您沒有提供查詢類型,也就是說,如果您的關鍵字引數不包含雙底線,則會假定查詢類型為 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")

大致翻譯成以下 SQL

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

請注意,這會匹配標題 'Today Lennon honored',但不會匹配 'today lennon honored'

還有一個不區分大小寫的版本,icontains

startswithendswith

分別為開頭和結尾搜尋。還有不區分大小寫的版本,分別稱為 istartswithiendswith

同樣,這只是冰山一角。完整的參考可以在欄位查詢參考中找到。

跨越關係的查詢

Django 提供了一種強大且直觀的方式來「追蹤」查詢中的關係,在幕後自動處理 SQL JOIN。要跨越關係,請使用模型之間相關欄位的欄位名稱,以雙底線分隔,直到到達您想要的欄位為止。

此範例檢索所有 Blogname'Beatles Blog'Entry 物件。

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

這種跨越可以像您希望的那樣深入。

它也可以反向運作。雖然它 可以 自訂,但預設情況下,您可以使用模型的小寫名稱來參考查詢中的「反向」關係。

此範例檢索所有具有至少一個 headline 包含 'Lennon'EntryBlog 物件。

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

如果您正在跨多個關係篩選,並且其中一個中間模型沒有符合篩選條件的值,Django 會將其視為存在一個空(所有值都是 NULL),但有效的物件。這表示不會引發任何錯誤。例如,在這個篩選器中

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

(如果存在相關的 Author 模型),如果沒有與條目關聯的 author,則會將其視為沒有附加的 name,而不是因為遺失的 author 而引發錯誤。通常這正是您想要發生的事情。唯一可能令人困惑的情況是,如果您使用 isnull。因此

Blog.objects.filter(entry__authors__name__isnull=True)

將傳回在 author 上具有空 nameBlog 物件,以及在 entry 上具有空 author 的物件。如果您不想要後面的那些物件,您可以寫成

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

跨越多值關係

當跨越 ManyToManyField 或反向 ForeignKey 時(例如從 BlogEntry),在多個屬性上篩選會引發是否要求每個屬性在同一相關物件中一致的問題。我們可能會尋找在標題中有 “Lennon” 的 2008 年條目的部落格,或者我們可能會尋找僅具有 2008 年條目以及一些更新或更舊的標題中包含 “Lennon” 的條目的部落格。

要選取所有包含至少一個 2008 年且標題中包含 “Lennon” 的條目的部落格(同一個條目滿足兩個條件),我們將寫成

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

否則,要執行更寬鬆的查詢,選取標題中僅具有某個條目包含 “Lennon”某個 2008 年條目的任何部落格,我們將寫成

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

假設只有一個部落格同時具有包含 “Lennon” 的條目和 2008 年的條目,但 2008 年的條目沒有一個包含 “Lennon”。第一個查詢不會傳回任何部落格,但第二個查詢會傳回該一個部落格。(這是因為第二個篩選器選取的條目可能與第一個篩選器中的條目相同,也可能不同。我們使用每個篩選器語句篩選 Blog 項目,而不是 Entry 項目。)簡而言之,如果每個條件都需要匹配同一個相關物件,則每個條件都應該包含在單個 filter() 呼叫中。

注意

第二個(較寬鬆的)查詢鏈接多個篩選器時,它會對主要模型執行多次聯結,可能會產生重複的結果。

>>> 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() 的行為與 exclude() 的實作方式並不相同。相反地,單一 exclude() 呼叫中的條件不一定會參照到相同的項目。

舉例來說,以下查詢會排除在標題中*「Lennon」*且發布於 2008 年的*所有*文章的網誌。

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

但是,與使用 filter() 時的行為不同,這不會根據同時滿足兩個條件的文章來限制網誌。為了實現這一點,也就是選擇所有不包含以*「Lennon」*發布且在 2008 年發布的文章的網誌,您需要進行兩個查詢。

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

篩選器可以參照模型上的欄位

到目前為止給出的範例中,我們建構的篩選器是將模型欄位的值與常數進行比較。但是,如果您想將模型欄位的值與同一模型上的另一個欄位進行比較呢?

Django 提供了 F 表達式 來允許這種比較。F() 的實例在查詢中充當模型欄位的參考。然後,這些參考可以用於查詢篩選器中,以比較同一模型實例上兩個不同欄位的值。

例如,要尋找所有評論數多於引用數的網誌文章列表,我們建構一個 F() 物件來參考引用數,並在查詢中使用該 F() 物件。

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

Django 支援使用加法、減法、乘法、除法、模數和冪算術與 F() 物件,包括使用常數和其他 F() 物件。要尋找所有評論數是引用數兩倍以上的網誌文章,我們修改查詢。

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

要尋找所有文章的評分低於引用數和評論數總和的文章,我們發出以下查詢。

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

您也可以使用雙底線表示法來跨越 F() 物件中的關係。帶有雙底線的 F() 物件將引入任何存取相關物件所需的聯結。例如,要檢索所有作者姓名與網誌名稱相同的文章,我們可以發出以下查詢。

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

對於日期和日期/時間欄位,您可以新增或減去 timedelta 物件。以下內容會傳回所有在發布後 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)

Oracle

Oracle 不支援位元 XOR 運算。

表達式可以參照轉換

Django 支援在表達式中使用轉換。

例如,要尋找所有發布年份與上次修改年份相同的 Entry 物件。

>>> from django.db.models import F
>>> Entry.objects.filter(pub_date__year=F("mod_date__year"))

要尋找文章最早發布的年份,我們可以發出以下查詢。

>>> from django.db.models import Min
>>> Entry.objects.aggregate(first_published_year=Min("pub_date__year"))

此範例尋找每個年份的最高評分文章值和所有文章的評論總數。

>>> from django.db.models import OuterRef, Subquery, Sum
>>> 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 查找快捷方式,代表「主鍵」。

在範例 Blog 模型中,主鍵是 id 欄位,因此以下三個陳述式是等效的。

>>> 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 組合,以在模型的主鍵上執行查詢。

# 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 查找也適用於聯結。例如,以下三個陳述式是等效的。

>>> 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 陳述式中逸出百分比符號和底線

等效於 LIKE SQL 陳述式 (iexactcontainsicontainsstartswithistartswithendswithiendswith) 的欄位查找會自動逸出在 LIKE 陳述式中使用的兩個特殊字元 — 百分比符號和底線。(在 LIKE 陳述式中,百分比符號表示多字元萬用字元,而底線表示單字元萬用字元。)

這表示事情應該直覺地運作,因此抽象不會洩漏。例如,要檢索所有包含百分比符號的文章,請像其他字元一樣使用百分比符號。

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

Django 會為您處理引號問題;產生的 SQL 看起來會像這樣。

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

底線也是如此。百分比符號和底線都會為您透明地處理。

快取和 QuerySet

每個 QuerySet 都包含一個快取,以最大程度地減少資料庫存取。了解它的運作方式將使您可以編寫最有效率的程式碼。

在新建的 QuerySet 中,快取是空的。第一次評估 QuerySet 時 — 因此,發生資料庫查詢時 — Django 會將查詢結果儲存在 QuerySet 的快取中,並傳回明確要求的結果(例如,如果 QuerySet 正在被迭代,則為下一個元素)。後續對 QuerySet 的評估會重複使用快取的結果。

請記住這種快取行為,因為如果您不正確地使用 QuerySet,它可能會讓您失望。例如,以下程式碼會建立兩個 QuerySet、評估它們,然後將它們丟棄。

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

這表示相同的資料庫查詢將執行兩次,實際上使您的資料庫負載加倍。此外,這兩個列表可能不包含相同的資料庫記錄,因為在兩個請求之間的瞬間,可能已新增或刪除 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])  # Reuse the cache from the evaluation.

何時不快取 QuerySet

查詢集並不總是快取其結果。當僅評估查詢集的*一部分*時,會檢查快取,但是如果未填入快取,則後續查詢傳回的項目不會被快取。具體來說,這表示使用陣列切片或索引 限制查詢集 不會填入快取。

例如,重複取得查詢集物件中的特定索引,每次都會查詢資料庫。

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

但是,如果已評估整個查詢集,則會改為檢查快取。

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

以下是一些會導致評估整個查詢集並因此填入快取的其他動作範例。

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

注意

單純列印查詢集不會填入快取。這是因為呼叫 __repr__() 僅傳回整個查詢集的切片。

非同步查詢

如果您正在撰寫非同步視圖或程式碼,您無法像上述描述的那樣使用 ORM 進行查詢,因為您不能從非同步程式碼中呼叫阻塞式同步程式碼 - 它會阻塞事件迴圈(或者,更有可能的是,Django 會注意到並引發 SynchronousOnlyOperation 來阻止這種情況發生)。

幸運的是,您可以使用 Django 的非同步查詢 API 執行許多查詢。每個可能會阻塞的方法(例如 get()delete())都有一個非同步變體 (aget()adelete()),當您遍歷結果時,您可以使用非同步迭代 (async for) 來替代。

查詢迭代

遍歷查詢的預設方式(使用 for)會在幕後產生一個阻塞式的資料庫查詢,因為 Django 會在迭代時載入結果。為了修正此問題,您可以切換到 async for

async for entry in Authors.objects.filter(name__startswith="A"):
    ...

請注意,您也不能執行其他可能會遍歷查詢集的操作,例如將 list() 包裹在它周圍以強制其評估(如果需要,您可以在 comprehension 中使用 async for)。

由於像 filter()exclude() 這樣的 QuerySet 方法實際上不會執行查詢 - 它們會在遍歷時設定要執行的查詢集 - 因此您可以自由地在非同步程式碼中使用這些方法。若要了解哪些方法可以繼續像這樣使用,以及哪些方法具有非同步版本,請閱讀下一節。

QuerySet 和管理員方法

管理員和查詢集上的一些方法(例如 get()first())會強制執行查詢集並且會阻塞。有些方法(例如 filter()exclude())不會強制執行,因此可以安全地從非同步程式碼中執行。但是您應該如何分辨差異呢?

雖然您可以四處查看是否有以 a 作為前綴的方法(例如,我們有 aget() 但沒有 afilter()),但有一個更合乎邏輯的方法 - 在 QuerySet 參考中查找它是哪種類型的方法。

在那裡,您會發現 QuerySets 上的方法分為兩個部分

  • 返回新查詢集的方法:這些是非阻塞的方法,並且沒有非同步版本。您可以在任何情況下自由使用這些方法,但在使用 defer()only() 之前,請閱讀相關說明。

  • 不返回查詢集的方法:這些是阻塞的方法,並且具有非同步版本 - 每個非同步方法的名稱都在其文件中註明,但我們的標準模式是新增 a 前綴。

使用這種區別,您可以找出何時需要使用非同步版本,以及何時不需要。例如,以下是一個有效的非同步查詢

user = await User.objects.filter(username=my_input).afirst()

filter() 返回一個查詢集,因此可以安全地在非同步環境中繼續串聯它,而 first() 會評估並返回一個模型實例 - 因此,我們變更為 afirst(),並在整個表達式的前面使用 await,以便以非同步友善的方式呼叫它。

注意

如果您忘記加入 await 部分,您可能會看到像「協程物件沒有屬性 x」「<協程 …>」字串代替您的模型實例之類的錯誤。如果您看到這些錯誤,表示您遺漏了某處的 await,以將該協程轉換為真實值。

交易

目前,非同步查詢和更新支援交易。您會發現嘗試使用交易會引發 SynchronousOnlyOperation

如果您想要使用交易,我們建議您在單獨的同步函數中撰寫 ORM 程式碼,然後使用 sync_to_async 呼叫它 - 請參閱 非同步支援 以取得更多資訊。

查詢 JSONField

JSONField 中的查詢實作方式不同,主要是因為存在鍵轉換。為了示範,我們將使用以下範例模型

from django.db import models


class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = models.JSONField(null=True)

    def __str__(self):
        return self.name

儲存和查詢 None

與其他欄位一樣,將 None 儲存為欄位的值會將其儲存為 SQL NULL。雖然不建議這麼做,但可以使用 Value(None, JSONField()) 來儲存 JSON 純量 null 而不是 SQL NULL

無論儲存哪個值,從資料庫擷取時,JSON 純量 null 的 Python 表示方式與 SQL NULL 相同,即 None。因此,可能很難區分它們。

這僅適用於 None 作為欄位的最上層值。如果 Nonelistdict 內,它將始終被解讀為 JSON null

在查詢時,None 值將始終被解讀為 JSON null。若要查詢 SQL NULL,請使用 isnull

>>> Dog.objects.create(name="Max", data=None)  # SQL NULL.
<Dog: Max>
>>> Dog.objects.create(name="Archie", data=Value(None, JSONField()))  # JSON null.
<Dog: Archie>
>>> Dog.objects.filter(data=None)
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data=Value(None, JSONField()))
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data__isnull=True)
<QuerySet [<Dog: Max>]>
>>> Dog.objects.filter(data__isnull=False)
<QuerySet [<Dog: Archie>]>

除非您確定要處理 SQL NULL 值,否則請考慮設定 null=False,並為空值提供合適的預設值,例如 default=dict

注意

儲存 JSON 純量 null 不會違反 null=False

鍵、索引和路徑轉換

若要根據給定的字典鍵進行查詢,請使用該鍵作為查詢名稱

>>> Dog.objects.create(
...     name="Rufus",
...     data={
...         "breed": "labrador",
...         "owner": {
...             "name": "Bob",
...             "other_pets": [
...                 {
...                     "name": "Fishy",
...                 }
...             ],
...         },
...     },
... )
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": None})
<Dog: Meg>
>>> Dog.objects.filter(data__breed="collie")
<QuerySet [<Dog: Meg>]>

可以將多個鍵串聯在一起以形成路徑查詢

>>> Dog.objects.filter(data__owner__name="Bob")
<QuerySet [<Dog: Rufus>]>

如果鍵是整數,則會將其解讀為陣列中的索引轉換

>>> Dog.objects.filter(data__owner__other_pets__0__name="Fishy")
<QuerySet [<Dog: Rufus>]>

如果您要查詢的鍵與另一個查詢的名稱衝突,請改用 contains 查詢。

若要查詢遺失的鍵,請使用 isnull 查詢

>>> Dog.objects.create(name="Shep", data={"breed": "collie"})
<Dog: Shep>
>>> Dog.objects.filter(data__owner__isnull=True)
<QuerySet [<Dog: Shep>]>

注意

上面給出的查詢範例隱含地使用 exact 查詢。鍵、索引和路徑轉換也可以與以下項目串聯:icontainsendswithiendswithiexactregexiregexstartswithistartswithltltegtgte,以及 包含和鍵查詢

KT() 表達式

class KT(lookup)

代表 JSONField 的鍵、索引或路徑轉換的文字值。您可以在 lookup 中使用雙底線表示法來串聯字典鍵和索引轉換。

例如

>>> from django.db.models.fields.json import KT
>>> Dog.objects.create(
...     name="Shep",
...     data={
...         "owner": {"name": "Bob"},
...         "breed": ["collie", "lhasa apso"],
...     },
... )
<Dog: Shep>
>>> Dogs.objects.annotate(
...     first_breed=KT("data__breed__1"), owner_name=KT("data__owner__name")
... ).filter(first_breed__startswith="lhasa", owner_name="Bob")
<QuerySet [<Dog: Shep>]>

注意

由於鍵路徑查詢的運作方式,exclude()filter() 無法保證產生完整的集合。如果您想包含沒有該路徑的物件,請加入 isnull 查詢條件。

警告

由於任何字串都可能是 JSON 物件中的鍵,因此除了下面列出的查詢條件之外,任何其他查詢條件都會被解釋為鍵查詢。不會引發任何錯誤。請特別注意打字錯誤,並始終檢查您的查詢是否按您的意圖運作。

MariaDB 和 Oracle 使用者

在鍵、索引或路徑轉換上使用 order_by() 將會使用值的字串表示形式對物件進行排序。這是因為 MariaDB 和 Oracle 資料庫沒有提供將 JSON 值轉換為對等 SQL 值的函數。

Oracle 使用者

在 Oracle 資料庫上,在 exclude() 查詢中使用 None 作為查詢值,將會返回在給定路徑上沒有 null 值的物件,包括沒有該路徑的物件。在其他資料庫後端,查詢將返回具有該路徑且值不是 null 的物件。

PostgreSQL 使用者

在 PostgreSQL 上,如果只使用一個鍵或索引,則會使用 SQL 運算子 ->。如果使用多個運算子,則會使用 #> 運算子。

SQLite 使用者

在 SQLite 上,字串值 "true""false""null" 將始終被解釋為 TrueFalse 和 JSON null

包含和鍵查詢

contains

JSONField 上會覆寫 contains 查詢條件。返回的物件是指那些給定的鍵值對 dict 全部包含在欄位的頂層的物件。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador", "owner": "Bob"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.create(name="Fred", data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contains={"owner": "Bob"})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>
>>> Dog.objects.filter(data__contains={"breed": "collie"})
<QuerySet [<Dog: Meg>]>

Oracle 和 SQLite

Oracle 和 SQLite 不支援 contains

contained_by

這是 contains 查詢條件的反向 - 返回的物件將是那些物件上的鍵值對是傳遞的值中的子集的物件。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador", "owner": "Bob"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.create(name="Fred", data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contained_by={"breed": "collie", "owner": "Bob"})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>
>>> Dog.objects.filter(data__contained_by={"breed": "collie"})
<QuerySet [<Dog: Fred>]>

Oracle 和 SQLite

Oracle 和 SQLite 不支援 contained_by

has_key

返回資料頂層中具有給定鍵的物件。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.filter(data__has_key="owner")
<QuerySet [<Dog: Meg>]>

has_keys

返回資料頂層中具有所有給定鍵的物件。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.filter(data__has_keys=["breed", "owner"])
<QuerySet [<Dog: Meg>]>

has_any_keys

返回資料頂層中具有任何給定鍵的物件。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.filter(data__has_any_keys=["owner", "breed"])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

使用 Q 物件的複雜查詢條件

關鍵字引數查詢 – 在 filter() 等函數中 – 會被「AND」在一起。如果您需要執行更複雜的查詢(例如,帶有 OR 語句的查詢),您可以使用 Q 物件

Q 物件django.db.models.Q)是一個用於封裝關鍵字引數集合的物件。這些關鍵字引數的指定方式與上面的「欄位查詢條件」相同。

例如,這個 Q 物件封裝了一個單一的 LIKE 查詢:

from django.db.models import Q

Q(question__startswith="What")

可以使用 &|^ 運算子組合 Q 物件。當運算子用於兩個 Q 物件時,它會產生一個新的 Q 物件。

例如,此陳述式產生一個單一的 Q 物件,表示兩個 "question__startswith" 查詢的「OR」:

Q(question__startswith="Who") | Q(question__startswith="What")

這等效於以下 SQL WHERE 子句:

WHERE question LIKE 'Who%' OR question LIKE 'What%'

您可以透過組合 Q 物件與 &|^ 運算子,以及使用括號分組,來組合任意複雜的陳述式。此外,可以使用 ~ 運算子對 Q 物件進行否定,從而允許組合查詢,將一般查詢和否定 (NOT) 查詢結合起來。

Q(question__startswith="Who") | ~Q(pub_date__year=2005)

每個接收關鍵字引數的查詢函數(例如,filter()exclude()get())也可以傳遞一個或多個 Q 物件作為位置(非命名)引數。如果您向查詢函數提供多個 Q 物件引數,這些引數將會被「AND」在一起。例如:

Poll.objects.get(
    Q(question__startswith="Who"),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)

... 大致轉換為 SQL:

SELECT * from polls WHERE question LIKE 'Who%'
    AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

查詢函數可以混合使用 Q 物件和關鍵字引數。提供給查詢函數的所有引數(無論是關鍵字引數還是 Q 物件)都會被「AND」在一起。但是,如果提供了 Q 物件,它必須在任何關鍵字引數的定義之前。例如:

Poll.objects.get(
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
    question__startswith="Who",
)

... 會是一個有效的查詢,等效於前一個範例;但是:

# INVALID QUERY
Poll.objects.get(
    question__startswith="Who",
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)

... 會是無效的。

另請參閱

Django 單元測試中的 OR 查詢條件範例展示了 Q 的一些可能用法。

比較物件

要比較兩個模型實例,請使用標準 Python 比較運算子,雙等號:==。在幕後,它比較兩個模型的主鍵值。

使用上面的 Entry 範例,以下兩個陳述式是等效的:

>>> some_entry == other_entry
>>> some_entry.id == other_entry.id

如果模型的主鍵不是名為 id,也沒有問題。比較將始終使用主鍵,無論它叫什麼名稱。例如,如果模型的主鍵欄位名為 name,則以下兩個陳述式是等效的:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name

刪除物件

刪除方法很方便地命名為 delete()。此方法會立即刪除物件,並傳回已刪除的物件數量以及一個字典,其中包含每個物件類型的刪除數量。範例:

>>> e.delete()
(1, {'blog.Entry': 1})

您也可以大量刪除物件。每個 QuerySet 都有一個 delete() 方法,該方法會刪除該 QuerySet 的所有成員。

例如,這會刪除所有 pub_date 年份為 2005 的 Entry 物件:

>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})

請記住,這將盡可能純粹地在 SQL 中執行,因此在過程中不一定會呼叫個別物件實例的 delete() 方法。如果您在模型類別上提供了自訂的 delete() 方法,並且想要確保該方法被呼叫,您需要「手動」刪除該模型的實例(例如,透過迭代 QuerySet 並對每個物件個別呼叫 delete()),而不是使用 QuerySet 的批次 delete() 方法。

當 Django 刪除一個物件時,預設情況下會模擬 SQL 限制 ON DELETE CASCADE 的行為 – 換句話說,任何具有指向要刪除物件的外鍵的物件,都會與其一起刪除。例如

b = Blog.objects.get(pk=1)
# This will delete the Blog and all of its Entry objects.
b.delete()

此級聯行為可透過 ForeignKeyon_delete 參數進行自訂。

請注意,delete() 是唯一一個沒有在 Manager 本身公開的 QuerySet 方法。這是一種安全機制,可防止您意外請求 Entry.objects.delete(),並刪除所有條目。如果您確實想要刪除所有物件,則必須明確請求完整的查詢集

Entry.objects.all().delete()

複製模型實例

雖然沒有內建的方法可以複製模型實例,但可以輕鬆建立一個新的實例,並複製所有欄位的值。在最簡單的情況下,您可以將 pk 設定為 None,並將 _state.adding 設定為 True。使用我們的部落格範例

blog = Blog(name="My blog", tagline="Blogging is easy")
blog.save()  # blog.pk == 1

blog.pk = None
blog._state.adding = True
blog.save()  # blog.pk == 2

如果您使用繼承,情況會變得更複雜。考慮 Blog 的子類別

class ThemeBlog(Blog):
    theme = models.CharField(max_length=200)


django_blog = ThemeBlog(name="Django", tagline="Django is easy", theme="python")
django_blog.save()  # django_blog.pk == 3

由於繼承的工作方式,您必須將 pkid 都設定為 None,並將 _state.adding 設定為 True

django_blog.pk = None
django_blog.id = None
django_blog._state.adding = True
django_blog.save()  # django_blog.pk == 4

此過程不會複製不屬於模型資料庫表格的關聯。例如,Entry 具有與 AuthorManyToManyField。複製條目後,您必須為新條目設定多對多關聯

entry = Entry.objects.all()[0]  # some previous entry
old_authors = entry.authors.all()
entry.pk = None
entry._state.adding = True
entry.save()
entry.authors.set(old_authors)

對於 OneToOneField,您必須複製相關物件並將其指派給新物件的欄位,以避免違反一對一的唯一約束。例如,假設 entry 已如上所述複製

detail = EntryDetail.objects.all()[0]
detail.pk = None
detail._state.adding = True
detail.entry = entry
detail.save()

一次更新多個物件

有時您想要為 QuerySet 中的所有物件將欄位設定為特定值。您可以使用 update() 方法執行此操作。例如

# Update all the headlines with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline="Everything is the same")

您只能使用此方法設定非關聯欄位和 ForeignKey 欄位。要更新非關聯欄位,請提供新值作為常數。要更新 ForeignKey 欄位,請將新值設定為您想要指向的新模型實例。例如

>>> b = Blog.objects.get(pk=1)

# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.update(blog=b)

update() 方法會立即應用,並傳回查詢比對的列數(如果某些列已具有新值,則可能不等於更新的列數)。正在更新的 QuerySet 的唯一限制是它只能存取一個資料庫表格:模型的主要表格。您可以根據相關欄位進行篩選,但您只能更新模型主要表格中的欄。範例

>>> b = Blog.objects.get(pk=1)

# Update all the headlines belonging to this Blog.
>>> Entry.objects.filter(blog=b).update(headline="Everything is the same")

請注意,update() 方法會直接轉換為 SQL 語句。它是用於直接更新的批次操作。它不會在您的模型上執行任何 save() 方法,或發出 pre_savepost_save 訊號(這是呼叫 save() 的結果),或遵守 auto_now 欄位選項。如果您想要儲存 QuerySet 中的每個項目,並確保在每個實例上呼叫 save() 方法,您不需要任何特殊函數來處理它。迴圈處理它們並呼叫 save()

for item in my_queryset:
    item.save()

呼叫 update 也可以使用 F 表達式 根據模型中另一個欄位的值來更新一個欄位。這對於根據目前的計數器值遞增計數器特別有用。例如,要遞增部落格中每個條目的回溯計數

>>> Entry.objects.update(number_of_pingbacks=F("number_of_pingbacks") + 1)

但是,與篩選和排除子句中的 F() 物件不同,當您在更新中使用 F() 物件時,無法引入聯結 – 您只能參考正在更新的模型本機欄位。如果您嘗試使用 F() 物件引入聯結,則會引發 FieldError

# This will raise a FieldError
>>> Entry.objects.update(headline=F("blog__name"))

回退到原始 SQL

如果您發現自己需要撰寫一個對於 Django 的資料庫對應器來說太複雜的 SQL 查詢,您可以回退到手動撰寫 SQL。Django 有幾個用於撰寫原始 SQL 查詢的選項;請參閱執行原始 SQL 查詢

最後,重要的是要注意,Django 資料庫層僅是您資料庫的介面。您可以透過其他工具、程式語言或資料庫框架存取您的資料庫;您的資料庫沒有任何 Django 特定的內容。

返回頂部