Django 2.0 模型層中 QuerySet 查詢操作介紹

wcode發表於2018-05-24

文章翻譯總結自:官方文件

查詢操作

一旦建立了資料模型,Django 就會自動為您提供一個資料庫抽象 API,使您可以建立,檢索,更新和刪除物件。本文件介紹瞭如何使用此 API。

在本指南(和參考文獻)中,我們將參考以下模型,它們構成了一個 Weblog 應用程式:

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()
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()

    def __str__(self):
        return self.headline
複製程式碼

建立物件

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

要建立一個物件,請使用模型類的關鍵字引數對其進行例項化,然後呼叫 save() 以將其儲存到資料庫。

假設模型存在於檔案 mysite/blog/models.py 中,下面是一個例子:

>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()
複製程式碼

幕後執行 SQL 語句 INSERT 。 在你沒有呼叫 save() 方法之前, Django 不會將資料儲存到資料庫

save() 方法沒有返回值。

儲存對物件的更改

要儲存對已存在於資料庫中的物件的更改,請使用 save()

給定一個已經儲存到資料庫的 Blog 例項 b5,這個例子改變它的名字並更新資料庫中的記錄:

>>> b5.name = 'New name'
>>> b5.save()
複製程式碼

幕後執行 SQL 語句 UPDATE 。 在你沒有呼叫 save() 方法之前, Django 不會將資料儲存到資料庫

儲存 ForeignKey 和 ManyToManyField 欄位

更新 ForeignKey 欄位的方式與儲存普通欄位的方式完全相同 -- 只需將正確型別的物件分配給相關欄位即可。

>>> 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()
複製程式碼

還可以通過 add() 方法:

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)
複製程式碼

要一次新增多個記錄:

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

QuerySet 表示資料庫中的物件集合。它可以有零個,一個或多個過濾器。過濾器根據給定的引數縮小查詢結果的範圍。在 SQL 術語中,QuerySet 等同於 SELECT 語句,過濾器是限制性子句,如 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 是模型的 QuerySet 的主要來源。例如,Blog.objects.all() 返回包含資料庫中所有 Blog 物件的 QuerySet

檢索所有物件

從表中檢索物件的最簡單方法是獲取所有物件。為此,請使用 Manager 上的 all() 方法:

>>> all_entries = Entry.objects.all()
複製程式碼

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

使用過濾器檢索特定的物件

filter(**kwargs)

返回包含匹配給定查詢引數的物件的新 QuerySet

exclude(**kwargs)

返回包含與給定查詢引數不匹配的物件的新 QuerySet

例如,要獲取從 2006 年開始的部落格條目的 QuerySet,請使用 filter(),如下所示:

Entry.objects.filter(pub_date__year=2006)
複製程式碼

使用預設 manager 類,它與以下內容相同:

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 時,都會獲得全新的 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 會延遲查詢(lazy)

QuerySets 是懶惰的 -- 建立 QuerySet 的行為不涉及任何資料庫活動。您可以將過濾器堆疊在一起,並且在使用 QuerySet 之前,Django 不會真正執行查詢。看看這個例子:

>>> 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(p) 時才會真正執行一次資料庫查詢操作。

用 get() 檢索單個物件

filter() 總是返回一個 QuerySet,即使匹配的物件只有一個。

如果你知道只有一個物件和查詢匹配,則可以直接使用 get() 方法返回單個物件:

>>> one_entry = Entry.objects.get(pk=1)
複製程式碼

!> 請注意,get() 方法如果沒有查詢到物件,將引發 DoesNotExist 異常。查詢到多個物件則會引發 MultipleObjectReturned 異常,這兩個異常都是模型類的一個屬性。

擷取 QuerySet

可以使用 Python 中的切片語法對 QuerySet 的數量進行限制。這相當於 SQL LIMIT 和 OFFSET 子句。

例如,返回前 5 個物件(LIMIT 5)。

>>> Entry.objects.all()[:5]
複製程式碼

返回第六到第十個物件(OFFSET 5 LIMIT 5)。

>>> Entry.objects.all()[5:10]
複製程式碼

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

通常切片一個 QuerySet 會返回一個新的 QuerySet,但這不會真的執行資料庫操作。但是如果使用了 step 切片語法 例如下面這樣,則是會執行資料庫操作的:

>>> Entry.objects.all()[:10:2]
複製程式碼

由於切片的工作原理是模糊的,所以禁止對切片後的 queryset 進行進一步的過濾或排序。

要檢索單個物件而不是列表時,請使用下標索引而不是切片,例如:

>>> Entry.objects.order_by('headline')[0]
複製程式碼

這大致相當於:

>>> Entry.objects.order_by('headline')[0:1].get()
複製程式碼

請注意,如果沒有檢索到對應的物件,前者會引發 indexError,後者會引發 DoesNotExist

欄位查詢

欄位查詢是指你如何指定 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';
複製程式碼

在查詢中指定的欄位必須是模型欄位的名稱。但是有一個例外,在使用 ForeignKey 的情況下,你可以指定帶 _id 字尾的欄位名稱。在這種情況下, value 引數應該包含外部模型主鍵的原始值。例如:

>>> Entry.objects.filter(blog_id=4)
複製程式碼

如果您傳遞了無效的關鍵字引數,則會引發 TypeError

一些常見的查詢術語:

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")
複製程式碼

將匹配 name"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

startswith, endswith

分別匹配開始和結尾部分。也有不區分大小寫的版本 istartswithiendswith

跨越關係查詢

Django 提供了一種強大且直觀的方式來在查詢中 “追蹤” 關係,在幕後自動為您處理 SQL JOIN。要跨越關係,只需使用模型中相關欄位的欄位名稱(用雙下劃線分隔),直到您到達所需的欄位。

本示例使用 name'Beatles Blog'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 上具有空 AuthorBlog 物件。如果你不想要後者,你可以這樣寫:

Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)
複製程式碼

跨越多值關係

當您基於 ManyToManyField 或反向 ForeignKey 過濾物件時,您可能會感興趣的是兩種不同型別的過濾器。考慮 Blog/Entry 關係(Blog to Entry 是一對多的關係)。我們可能會感興趣的是找到一個有 entryblogheadline 中有 “Lennon”,並於 2008 年發表。或者,我們可能想要找到在 headline 中有 “Lennon” entryblog,以及 2008 年發表的一篇文章。由於有多個 entry 與單個 Blog 相關聯,所以這些查詢都是可能的,並且在某些情況下是有意義的。

同樣的情況也出現在 ManyToManyField 上。例如,如果一個 Entry 有一個 ManyToManyFieldtags,我們可能想要找到連結到名為 “music” 和 “bands” 的 tagentry,或者我們可能需要包含 name 為 “music” 和 狀態為 “public” 的 tagentry

為了處理這兩種情況,Django 擁有一致的處理 filter() 呼叫的方法。同時應用單個 filter() 呼叫中的所有內容以篩選出滿足所有這些要求的專案。連續的 filter() 呼叫進一步限制了物件集合,但對於多值關係,它們適用於連結到主模型的任何物件,而不一定是由較早的 filter() 呼叫選擇的那些物件。

這聽起來有點令人困惑,所以希望有一個例子可以說明。要選擇包含 headline 為 “Lennon” 並且在 2008 年釋出的 entry(滿足這兩個條件的同一 entry)的所有 blog,我們會寫:

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)
複製程式碼

要選擇 headline 中包含 “Lennon” entry 的所有 blog 以及 2008 年發表的 entry,我們將編寫:

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
複製程式碼

假設只有一個 blog 有包含 “Lennon” 的 entry 和 2008 年的 entry ,但是 2008 年的 entry 中沒有一個包含 “Lennon”。第一個查詢不會返回任何 blog,但第二個查詢將返回該 blog

在第二個示例中,第一個過濾器將查詢集限制為連結到 headline 中帶有 “Lennon” entry 的所有 blog。第二個過濾器將這組 blog 進一步限制為那些也連結到 2008 年釋出的 entryblog。第二個過濾器選擇的 entry 可能與第一個過濾器中的 entry 相同或不同。我們使用每個過濾器語句過濾 Blog item,而不是 Entry item。

如上所述,跨越多值關係的查詢的 filter() 行為對於 exclude() 不等效。相反,單個 exclude() 呼叫中的條件不一定引用相同的 item。

例如,以下查詢將排除同時包含 2008 年釋出的 entryheadline 中包含 “Lennon” entryblog

Blog.objects.exclude(
    entry__headline__contains='Lennon',
    entry__pub_date__year=2008,
)
複製程式碼

但是,與使用 filter() 時的行為不同,這不會限制基於滿足這兩個條件的 entryblog。為了做到這一點,例如,選擇所有不包含 2008 年出版的 “Lennon” entryblog,你需要做兩個查詢:

Blog.objects.exclude(
    entry__in=Entry.objects.filter(
        headline__contains='Lennon',
        pub_date__year=2008,
    ),
)
複製程式碼

過濾器可以引用模型上的欄位

在迄今為止給出的例子中,我們構建了一個過濾器,它將模型欄位的值與常量進行比較。但是如果您想要將模型欄位的值與同一模型中的另一個欄位進行比較呢?

Django提供了 F 表示式 來允許這樣的比較。F() 的例項充當對查詢中的模型欄位的引用。然後可以在查詢過濾器中使用這些引用來比較同一模型例項上兩個不同欄位的值。

例如,要查詢比 pingbacks 有更多註釋的所有 blog entry 的列表,我們構造一個 F() 物件來引用 pingback 計數,並在查詢中使用該 F() 物件:

>>> from django.db.models import F
>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks'))
複製程式碼

Django 支援對 F() 物件使用常量和其他 F() 物件的加法,減法,乘法,除法,模和冪運算。為了找到所有比 pingbacks 多兩倍的評論,我們修改查詢:

>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks') * 2)
複製程式碼

要查詢 entry 評級小於 pingback 計數和評論計數總和的所有條目,我們將發出查詢:

>>> Entry.objects.filter(rating__lt=F('n_comments') + F('n_pingbacks'))
複製程式碼

您還可以使用雙下劃線表示法來跨越 F() 物件中的關係。具有雙下劃線的 F() 物件將引入訪問相關物件所需的任何連線。例如,要檢索作者姓名與部落格名稱相同的所有條目,我們可以發出查詢:

>>> Entry.objects.filter(authors__name=F('blog__name'))
複製程式碼

對於 date 和 date/time 欄位,可以新增或減去 timedelta 物件。以下內容將返回釋出後超過 3 天修改的所有 entry

>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))
複製程式碼

F() 物件支援 .bitand().bitor().bitrightshift().bitleftshift() 的按位運算。例如:

>>> F('somefield').bitand(16)
複製程式碼

通過 pk 查詢

為了方便起見,Django 提供了一個 pk 查詢快捷方式,代表 "primary key"。

在示例 Blog 模型中,primary key 是 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 語句(iexact,contains,icontains,startswith,istartswith,endswith 和 iendswith)的欄位查詢將自動轉義為 LIKE 語句中使用的兩個特殊字元 -- 百分號和下劃線。(在 LIKE 語句中,百分號表示多字元萬用字元,下劃線表示單字元萬用字元。)

這意味我們可以專注於業務邏輯,而不用考慮 SQL 語法。例如,要檢索包含百分號的所有條目,只需要直接使用百分號:

>>> 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]) # Re-use the cache from the evaluation.
複製程式碼

當 QuerySet 沒有被快取時

查詢集並不總是快取它們的結果。在僅操作部分查詢集時,會檢查快取,但如果未填充,則後續查詢返回的項不會被快取。具體而言,這意味著使用陣列切片或索引擷取查詢集不會填充快取。

例如,重複獲取 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__() 僅返回整個查詢集的一部分。

使用 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 比較運算子雙等號: ==。在底層,Django 將比較了兩個模型的 primary key。

使用上面的 Entry 例子,以下兩個語句是等價的:

>>> some_entry == other_entry
>>> some_entry.id == other_entry.id
複製程式碼

如果模型的 primary key 不是 id,也沒有問題。不管它叫什麼,比較總是使用 primary key。例如,如果模型的 primary key 欄位被稱為 name,則這兩個語句是等同的:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name
複製程式碼

刪除物件

方便的刪除方法被命名為 delete()。此方法立即刪除物件並返回刪除的物件數量和每個物件型別具有刪除次數的字典。例:

>>> e.delete()
(1, {'weblog.Entry': 1})
複製程式碼

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

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

>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})
複製程式碼

當 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。使用我們的部落格示例:

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

blog.pk = None
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

django_blog.pk = None
django_blog.id = None
django_blog.save() # django_blog.pk == 4
複製程式碼

此過程不會複製不屬於模型資料庫表的關係。Entry 有一個 ManyToManyField to Author。複製 entry 後,您必須設定新 entry 的多對多關係:

entry = Entry.objects.all()[0] # some previous entry
old_authors = entry.authors.all()
entry.pk = None
entry.save()
entry.authors.set(old_authors)
複製程式碼

對於 OneToOneField,您必須複製相關物件並將其分配給新物件的欄位,以避免違反一對一唯一約束。例如,假設 entry 已經被重複如上:

detail = EntryDetail.objects.all()[0]
detail.pk = None
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.all().update(blog=b)
複製程式碼

update() 方法立即應用並返回查詢匹配的行數(如果某些行已具有新值,則該行數不計算在更新的行數內)。QuerySet 被更新的唯一限制是它只能訪問一個資料庫表:模型的主表。您可以根據相關欄位進行過濾,但只能更新模型主表中的列。例:

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

# 更新屬於這個部落格的所有標題。
>>> Entry.objects.select_related().filter(blog=b).update(headline='Everything is the same')
複製程式碼

請注意 update() 方法直接轉換為 SQL 語句。這是直接更新的批量操作。它不會在模型上執行任何 save() 方法,或者發出 pre_savepost_save 訊號(這是呼叫 save() 的結果),或兌現 auto_now 欄位選項。如果要將每個 item 儲存在 QuerySet 中並確保在每個例項上呼叫 save() 方法,則不需要任何特殊函式來處理該 item。只需迴圈它們並呼叫 save()

for item in my_queryset:
    item.save()
複製程式碼

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

>>> Entry.objects.all().update(n_pingbacks=F('n_pingbacks') + 1)
複製程式碼

但是,與 filterexclude 子句中的 F() 物件不同,在 update 中使用 F() 物件時不能引入連線 -- 您只能引用正在更新的模型的本地欄位。如果您嘗試使用 F() 物件引入聯接,則會引發 FieldError

# This will raise a FieldError
>>> Entry.objects.update(headline=F('blog__name'))
複製程式碼

關係物件

當您在模型中定義關係(即 ForeignKeyOneToOneFieldManyToManyField )時,該模型的例項將具有訪問相關物件的方便的 API。

本節中的所有示例均使用本頁頂部定義的示例 BlogAuthorEntry 模型。

一對多的關係

正向訪問

如果模型具有 ForeignKey ,則該模型的例項將通過模型的簡單屬性訪問相關(外部)物件。

舉例:

>>> e = Entry.objects.get(id=2)
>>> e.blog # Returns the related Blog object.
複製程式碼

您可以通過外來鍵屬性操作外來鍵物件。在呼叫 save() 之前,對外來鍵的更改不會儲存到資料庫。例:

>>> e = Entry.objects.get(id=2)
>>> e.blog = some_blog
>>> e.save()
複製程式碼

如果 ForeignKey 欄位具有 null=True(即,它允許 NULL 值),則可以分配 None 以移除關係。例:

>>> e = Entry.objects.get(id=2)
>>> e.blog = None
>>> e.save() # "UPDATE blog_entry SET blog_id = NULL ...;"
複製程式碼

第一次訪問相關物件時,快取對一對多關係的正向訪問。隨後訪問同一物件例項上的外來鍵將被快取。例:

>>> e = Entry.objects.get(id=2)
>>> print(e.blog)  # Hits the database to retrieve the associated Blog.
>>> print(e.blog)  # Doesn't hit the database; uses cached version.
複製程式碼

請注意,select_related() QuerySet 方法會提前預先填充所有一對多關係的快取。例:

>>> e = Entry.objects.select_related().get(id=2)
>>> print(e.blog)  # Doesn't hit the database; uses cached version.
>>> print(e.blog)  # Doesn't hit the database; uses cached version.
複製程式碼

反向訪問

如果模型具有 ForeignKey,則外來鍵模型的例項將有權訪問 Manager,該 Manager 返回第一個模型的所有例項。預設情況下,此 Manager 名為 FOO_set,其中 FOO 是源模型名稱,小寫。該 Manager 返回 QuerySet,可以按照上述 “檢索物件” 一節中的描述進行過濾和操作。

舉例:

>>> b = Blog.objects.get(id=1)
>>> b.entry_set.all() # Returns all Entry objects related to Blog.

# b.entry_set is a Manager that returns QuerySets.
>>> b.entry_set.filter(headline__contains='Lennon')
>>> b.entry_set.count()
複製程式碼

您可以通過在 ForeignKey 定義中設定 related_name 引數來覆蓋 FOO_set 名稱。例如,如果 Entry 模型被更改為 blog=ForeignKey(Blog, on_delete=models.CASCADE, related_name='entries'),上面的示例程式碼將如下所示:

>>> b = Blog.objects.get(id=1)
>>> b.entries.all() # Returns all Entry objects related to Blog.

# b.entries is a Manager that returns QuerySets.
>>> b.entries.filter(headline__contains='Lennon')
>>> b.entries.count()
複製程式碼

使用自定義反向 manager

預設情況下,用於反向關係的 RelatedManager 是該模型的預設 manager 的子類。如果您想為給定的查詢指定不同的 manager,可以使用以下語法:

from django.db import models

class Entry(models.Model):
    #...
    objects = models.Manager()  # Default Manager
    entries = EntryManager()    # Custom Manager

b = Blog.objects.get(id=1)
b.entry_set(manager='entries').all()
複製程式碼

如果 EntryManager 在其 get_queryset() 方法中執行了預設過濾,則該過濾將應用於 all() 呼叫。

當然,指定一個自定義反向 manager 也可以讓你呼叫它的自定義方法:

b.entry_set(manager='entries').is_published()
複製程式碼

處理關係物件的其他方法

除了上面 “檢索物件” 中定義的 QuerySet 方法之外,ForeignKey Manager 還具有用於處理關係物件集的附加方法。

add(obj1, obj2, ...)

將特定的模型物件加入關聯物件集合。

create(**kwargs)

建立一個新物件,將其儲存並放入關係物件集中。返回新建立的物件。

remove(obj1, obj2, ...)

從關係物件集中刪除指定的模型物件。

clear()

從關係物件集中刪除所有物件。

set(objs)

替換一組相關的物件。

要分配相關集的成員,請將 set() 方法與可迭代的物件例項一起使用。例如,如果 e1e2Entry 例項:

b = Blog.objects.get(id=1)
b.entry_set.set([e1, e2])
複製程式碼

如果 clear() 方法可用,則在 iterable(本例中為列表)中的所有物件都新增到 set 之前,將從 entry_set 中刪除任何預先存在的物件。如果 clear() 方法不可用,則會新增迭代中的所有物件而不刪除任何現有元素。

本節中描述的每個 “反向” 操作對資料庫都有直接影響。每一次新增,建立和刪除都會立即自動儲存到資料庫中。

多對多的關係

多對多關係的兩端都可以自動訪問另一端的 API。API 的作用類似於上面的 “反向” 一對多關係。

一個區別在於屬性命名:定義 ManyToManyField 的模型使用該欄位本身的屬性名稱,而 “反向” 模型使用原始模型的小寫模型名稱和 “_set”(就像反向一對多關係)。

一個例子使得這更容易理解:

e = Entry.objects.get(id=3)
e.authors.all() # Returns all Author objects for this Entry.
e.authors.count()
e.authors.filter(name__contains='John')

a = Author.objects.get(id=5)
a.entry_set.all() # Returns all Entry objects for this Author.
複製程式碼

ForeignKey 一樣,ManyToManyField 可以指定 related_name。在上面的例子中,如果 Entry 中的 ManyToManyField 指定了 related_name='entries',那麼每個 Author 例項將具有 entries 屬性而不是 entry_set

與一對多關係的另一個區別是,除了模型例項之外,多對多關係中的 add()set()remove() 方法接受主鍵值。例如,如果 e1e2Entry 例項,那麼這些 set() 呼叫的工作原理是相同的:

a = Author.objects.get(id=5)
a.entry_set.set([e1, e2])
a.entry_set.set([e1.pk, e2.pk])
複製程式碼

一對一的關係

一對一關係與多對一關係非常相似。如果您在模型上定義了 OneToOneField,則該模型的例項將通過模型的簡單屬性訪問相關物件。

class EntryDetail(models.Model):
    entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
    details = models.TextField()

ed = EntryDetail.objects.get(id=2)
ed.entry # Returns the related Entry object.
複製程式碼

差異出現在 “反向” 查詢中。一對一關係中的相關模型也可以訪問 Manager 物件,但該 Manager 表示一個物件,而不是一組物件:

e = Entry.objects.get(id=2)
e.entrydetail # returns the related EntryDetail object
複製程式碼

如果沒有物件被分配給這個關係,Django 將引發一個 DoesNotExist 異常。

例項可以按照與分配轉發關係相同的方式分配給反向關係:

e.entrydetail = ed
複製程式碼

相關文章