1. 程式人生 > >查詢操作 -- Django從入門到精通系列教程

查詢操作 -- Django從入門到精通系列教程

該系列教程繫個人原創,並完整發布在個人官網劉江的部落格和教程

所有轉載本文者,需在頂部顯著位置註明原作者及www.liujiangblog.com官網地址。

查詢操作是Django的ORM框架中最重要的內容之一。我們建立模型、儲存資料為的就是在需要的時候可以查詢得到資料。Django自動為所有的模型提供了一套完善、方便、高效的API,一些重要的,我們要背下來,一些不常用的,要有印象,使用的時候可以快速查詢參考手冊。

本節的內容基於如下的一個部落格應用模型:

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=200)
    email = models.EmailField()

    def __str__(self):              # __unicode__ on Python 2
        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):              # __unicode__ on Python 2
        return self.headline

一、建立物件

假設模型位於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()方法沒有返回值,它可以接受一些額外的引數。

如果想要一行程式碼完成上面的操作,請使用creat()

方法,它可以省略save的步驟:

b = Blog.objects.create(name='Beatles Blog', tagline='All the latest Beatles news.')

二、儲存物件

使用save()方法,儲存對資料庫內已有物件的修改。例如如果已經存在b5物件在資料庫內:

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

在後臺,這會執行一條SQL的UPDATE語句。如果你不顯式地呼叫save()方法,Django不會立刻將該操作反映到資料庫中。

1. 儲存外來鍵和多對多欄位

儲存一個外來鍵欄位和儲存普通欄位沒什麼區別,只是要注意值的型別要正確。下面的例子,有一個Entry的例項entry和一個Blog的例項cheese_blog

,然後把cheese_blog作為值賦給了entry的blog屬性,最後呼叫save方法進行儲存。

>>> 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()方法,而不是直接給屬性賦值,但它不需要呼叫save方法。如下例所示:

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

如果你指定或添加了錯誤型別的物件,Django會丟擲異常。

三、檢索物件

想要從資料庫內檢索物件,你需要基於模型類,通過管理器(Manager)構造一個查詢結果集(QuerySet)。

每個QuerySet代表一些資料庫物件的集合。它可以包含零個、一個或多個過濾器(filters)。Filters縮小查詢結果的範圍。在SQL語法中,一個QuerySet相當於一個SELECT語句,而filter則相當於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."

1. 檢索所有物件

使用all()方法,可以獲取某張表的所有記錄。

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

2. 過濾物件

有兩個方法可以用來過濾QuerySet的結果,分別是:

  • filter(**kwargs):返回一個根據指定引數查詢出來的QuerySet
  • exclude(**kwargs):返回除了根據指定引數查詢出來結果的QuerySet

其中,**kwargs引數的格式必須是Django設定的一些欄位格式。

例如:

Entry.objects.filter(pub_date__year=2006)

它等同於:

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

鏈式過濾

filter和exclude的結果依然是個QuerySet,因此它可以繼續被filter和exclude,這就形成了鏈式過濾:

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

(這裡需要注意的是,當在進行跨關係的鏈式過濾時,結果可能和你想象的不一樣,參考下面的跨多值關係查詢)

被過濾的QuerySets都是唯一的

每一次過濾,你都會獲得一個全新的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())

例子中的q2和q3雖然由q1得來,是q1的子集,但是都是獨立自主存在的。同樣q1也不會受到q2和q3的影響。

QuerySets都是懶惰的

一個建立QuerySets的動作不會立刻導致任何的資料庫行為。你可以不斷地進行filter動作一整天,Django不會執行任何實際的資料庫查詢動作,直到QuerySets被提交(evaluated)。

簡而言之就是,只有碰到某些特定的操作,Django才會將所有的操作體現到資料庫內,否則它們只是儲存在記憶體和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)

上面的例子,看起來執行了3次資料庫訪問,實際上只是在print語句時才執行1次訪問。通常情況,QuerySets的檢索不會立刻執行實際的資料庫查詢操作,直到出現類似print的請求,也就是所謂的evaluated。

3. 檢索單一物件

filter方法始終返回的是QuerySets,那怕只有一個物件符合過濾條件,返回的也是包含一個物件的QuerySets,這是一個集合型別物件,你可以簡單的理解為Python列表,可迭代可迴圈可索引。

如果你確定你的檢索只會獲得一個物件,那麼你可以使用Manager的get()方法來直接返回這個物件。

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

在get方法中你可以使用任何filter方法中的查詢引數,用法也是一模一樣。

注意:使用get()方法和使用filter()方法然後通過[0]的方式分片,有著不同的地方。看似兩者都是獲取單一物件。但是,如果在查詢時沒有匹配到物件,那麼get()方法將丟擲DoesNotExist異常。這個異常是模型類的一個屬性,在上面的例子中,如果不存在主鍵為1的Entry物件,那麼Django將丟擲Entry.DoesNotExist異常。

類似地,在使用get()方法查詢時,如果結果超過1個,則會丟擲MultipleObjectsReturned異常,這個異常也是模型類的一個屬性。

所以:get()方法要慎用!

4. 其它QuerySet方法

大多數情況下,需要從資料庫中查詢物件時,使用all()、 get()、filter() 和exclude()就行。針對QuerySet的方法還有很多,都是一些相對高階的用法。

5. QuerySet使用限制

使用類似Python對列表進行切片的方法可以對QuerySet進行範圍取值。它相當於SQL語句中的LIMIT和OFFSET子句。參考下面的例子:

>>> Entry.objects.all()[:5]      # 返回前5個物件
>>> Entry.objects.all()[5:10]    # 返回第6個到第10個物件

注意:不支援負索引!例如 Entry.objects.all()[-1]是不允許的

通常情況,切片操作會返回一個新的QuerySet,並且不會被立刻執行。但是有一個例外,那就是指定步長的時候,查詢操作會立刻在資料庫內執行,如下:

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

若要獲取單一的物件而不是一個列表(例如,SELECT foo FROM bar LIMIT 1),可以簡單地使用索引而不是切片。例如,下面的語句返回資料庫中根據標題排序後的第一條Entry:

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

它相當於:

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

注意:如果沒有匹配到物件,那麼第一種方法會丟擲IndexError異常,而第二種方式會丟擲DoesNotExist異常。

也就是說在使用get和切片的時候,要注意查詢結果的元素個數。

6. 欄位查詢

欄位查詢其實就是filter()、exclude()和get()等方法的關鍵字引數。
其基本格式是:field__lookuptype=value注意其中是雙下劃線
例如:

>>> Entry.objects.filter(pub_date__lte='2006-01-01')
# 相當於:
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

其中的欄位必須是模型中定義的欄位之一。但是有一個例外,那就是ForeignKey欄位,你可以為其新增一個“_id”字尾(單下劃線)。這種情況下鍵值是外來鍵模型的主鍵原生值。例如:

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

如果你傳遞了一個非法的鍵值,查詢函式會丟擲TypeError異常。

Django的資料庫API支援20多種查詢型別,下面介紹一些常用的:

exact:

預設型別。如果你不提供查詢型別,或者關鍵字引數不包含一個雙下劃線,那麼查詢型別就是這個預設的exact。

>>> Entry.objects.get(headline__exact="Cat bites dog")
# 相當於
# SELECT ... WHERE headline = 'Cat bites dog';
# 下面兩個相當
>>> Blog.objects.get(id__exact=14)  # Explicit form
>>> Blog.objects.get(id=14)         # __exact is implied

iexact:

不區分大小寫。

>>> Blog.objects.get(name__iexact="beatles blog")
# 匹配"Beatles Blog", "beatles blog",甚至"BeAtlES blOG".

contains:

表示包含的意思!大小寫敏感!

Entry.objects.get(headline__contains='Lennon')
# 相當於
# SELECT ... WHERE headline LIKE '%Lennon%';
# 匹配'Today Lennon honored',但不匹配'today lennon honored'

icontains:

contains的大小寫不敏感模式。

startswith和endswith

以什麼開頭和以什麼結尾。大小寫敏感!

istartswith和iendswith

是不區分大小寫的模式。

7. 跨越關係查詢

Django提供了強大並且直觀的方式解決跨越關聯的查詢,它在後臺自動執行包含JOIN的SQL語句。要跨越某個關聯,只需使用關聯的模型欄位名稱,並使用雙下劃線分隔,直至你想要的欄位(可以鏈式跨越,無限跨度)。例如:

# 返回所有Blog的name為'Beatles Blog'的Entry物件
# 一定要注意,返回的是Entry物件,而不是Blog物件。
# objects前面用的是哪個class,返回的就是哪個class的物件。
>>> Entry.objects.filter(blog__name='Beatles Blog')

反之亦然,如果要引用一個反向關聯,只需要使用模型的小寫名!

# 獲取所有的Blog物件,前提是它所關聯的Entry的headline包含'Lennon'
>>> Blog.objects.filter(entry__headline__contains='Lennon')

如果你在多級關聯中進行過濾而且其中某個中間模型沒有滿足過濾條件的值,Django將把它當做一個空的(所有的值都為NULL)但是合法的物件,不會丟擲任何異常或錯誤。例如,在下面的過濾器中:

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

如果Entry中沒有關聯任何的author,那麼它將當作其沒有name,而不會因為沒有author 引發一個錯誤。通常,這是比較符合邏輯的處理方式。唯一可能讓你困惑的是當你使用isnull的時候:

Blog.objects.filter(entry__authors__name__isnull=True)

這將返回Blog物件,它關聯的entry物件的author欄位的name欄位為空,以及Entry物件的author欄位為空。如果你不需要後者,你可以這樣寫:

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

跨越多值的關係查詢

最基本的filter和exclude的關鍵字引數只有一個,這種情況很好理解。但是當關鍵字引數有多個,且是跨越外來鍵或者多對多的情況下,那麼就比較複雜,讓人迷惑了。我們看下面的例子:

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

這是一個跨外來鍵、兩個過濾引數的查詢。此時我們理解兩個引數之間屬於-與“and”的關係,也就是說,過濾出來的BLog物件對應的entry物件必須同時滿足上面兩個條件。這點很好理解。也就是說上面要求至少有一個entry同時滿足兩個條件

但是,看下面的用法:

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

把兩個引數拆開,放在兩個filter呼叫裡面,按照我們前面說過的鏈式過濾,這個結果應該和上面的例子一樣。可實際上,它不一樣,Django在這種情況下,將兩個filter之間的關係設計為-或“or”,這真是讓人頭疼。

多對多關係下的多值查詢和外來鍵foreignkey的情況一樣。

但是,更頭疼的來了,exclude的策略設計的又和filter不一樣!

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

這會排除headline中包含“Lennon”的Entry和在2008年釋出的Entry,中間是一個-和“or”的關係!

那麼要排除同時滿足上面兩個條件的物件,該怎麼辦呢?看下面:

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

(有沒有很坑爹的感覺?所以,建議在碰到跨關係的多值查詢時,儘量使用Q查詢)

8. 使用F表示式引用模型的欄位

到目前為止的例子中,我們都是將模型欄位與常量進行比較。但是,如果你想將模型的一個欄位與同一個模型的另外一個欄位進行比較該怎麼辦?

使用Django提供的F表示式!

例如,為了查詢comments數目多於pingbacks數目的Entry,可以構造一個F()物件來引用pingback數目,並在查詢中使用該F()物件:

>>> from django.db.models import F
>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks'))

Django支援對F()物件進行加、減、乘、除、取模以及冪運算等算術操作。兩個運算元可以是常數和其它F()物件。例如查詢comments數目比pingbacks兩倍還要多的Entry,我們可以這麼寫:

>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks') * 2)

為了查詢rating比pingback和comment數目總和要小的Entry,我們可以這麼寫:

>>> Entry.objects.filter(rating__lt=F('n_comments') + F('n_pingbacks'))

你還可以在F()中使用雙下劃線來進行跨表查詢。例如,查詢author的名字與blog名字相同的Entry:

>>> 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()4種位操作,例如:

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

9. 主鍵的快捷查詢方式:pk

pk就是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

可以聯合其他型別的引數:

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

可以跨表操作:

>>> Entry.objects.filter(blog__id__exact=3) 
>>> Entry.objects.filter(blog__id=3) 
>>> Entry.objects.filter(blog__pk=3)

當主鍵不是id的時候,請注意了!

10. 在LIKE語句中轉義百分符號和下劃線

在原生SQL語句中%符號有特殊的作用。Django幫你自動轉義了百分符號和下劃線,你可以和普通字元一樣使用它們,如下所示:

>>> Entry.objects.filter(headline__contains='%')
# 它和下面的一樣
# SELECT ... WHERE headline LIKE '%\%%';

11. 快取與查詢集

每個QuerySet都包含一個快取,用於減少對資料庫的實際操作。理解這個概念,有助於你提高查詢效率。

對於新建立的QuerySet,它的快取是空的。當QuerySet第一次被提交後,資料庫執行實際的查詢操作,Django會把查詢的結果儲存在QuerySet的快取內,隨後的對於該QuerySet的提交將重用這個快取的資料。

要想高效的利用查詢結果,降低資料庫負載,你必須善於利用快取。看下面的例子,這會造成2次實際的資料庫操作,加倍資料庫的負載,同時由於時間差的問題,可能在兩次操作之間資料被刪除或修改或新增,導致髒資料的問題:

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

為了避免上面的問題,好的使用方式如下,這隻產生一次實際的查詢操作,並且保持了資料的一致性:

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # 提交查詢
>>> print([p.pub_date for p in queryset]) # 重用查詢快取

何時不會被快取

有一些操作不會快取QuerySet,例如切片和索引。這就導致這些操作沒有快取可用,每次都會執行實際的資料庫查詢操作。例如:

>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # 查詢資料庫
>>> print(queryset[5]) # 再次查詢資料庫

但是,如果已經遍歷過整個QuerySet,那麼就相當於快取過,後續的操作則會使用快取,例如:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # 查詢資料庫
>>> print(queryset[5]) # 使用快取
>>> print(queryset[5]) # 使用快取

下面的這些操作都將遍歷QuerySet並建立快取:

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

注意:簡單的列印QuerySet並不會建立快取,因為__repr__()呼叫只返回全部查詢集的一個切片。

四、使用Q物件進行復雜查詢

普通filter函式裡的條件都是“and”邏輯,如果你想實現“or”邏輯怎麼辦?用Q查詢!

Q來自django.db.models.Q,用於封裝關鍵字引數的集合,可以作為關鍵字引數用於filter、exclude和get等函式。
例如:

from django.db.models import Q
Q(question__startswith='What')

可以使用“&”或者“|”或“~”來組合Q物件,分別表示與或非邏輯。它將返回一個新的Q物件。

Q(question__startswith='Who')|Q(question__startswith='What')
# 這相當於:
WHERE question LIKE 'Who%' OR question LIKE 'What%'

更多的例子:

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

你也可以這麼使用,預設情況下,以逗號分隔的都表示AND關係:

Poll.objects.get(
Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)
# 它相當於
# SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

當關鍵字引數和Q物件組合使用時,Q物件必須放在前面,如下例子:

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

如果關鍵字引數放在Q物件的前面,則會報錯。

五、比較物件

要比較兩個模型例項,只需要使用python提供的雙等號比較符就可以了。在後臺,其實比較的是兩個例項的主鍵的值。下面兩種方法是等同的:

>>> 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, {'weblog.Entry': 1})

也可以批量刪除。每個QuerySet都有一個delete()方法,它能刪除該QuerySet的所有成員。例如:

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

需要注意的是,有可能不是每一個物件的delete方法都被執行。如果你改寫了delete方法,為了確保物件被刪除,你必須手動迭代QuerySet進行逐一刪除操作。

當Django刪除一個物件時,它預設使用SQL的ON DELETE CASCADE約束,也就是說,任何有外來鍵指向要刪除物件的物件將一起被刪除。例如:

b = Blog.objects.get(pk=1)
# 下面的動作將刪除該條Blog和所有的它關聯的Entry物件
b.delete()

這種級聯的行為可以通過的ForeignKey的on_delete引數自定義。

注意,delete()是唯一沒有在管理器上暴露出來的方法。這是刻意設計的一個安全機制,用來防止你意外地請求類似Entry.objects.delete()的動作,而不慎刪除了所有的條目。如果你確實想刪除所有的物件,你必須明確地請求一個完全的查詢集,像下面這樣:

Entry.objects.all().delete()

七、複製模型例項

雖然沒有內建的方法用於複製模型的例項,但還是很容易建立一個新的例項並將原例項的所有欄位都拷貝過來。最簡單的方法是將原例項的pk設定為None,這會建立一個新的例項copy。示例如下:

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

基於繼承的工作機制,你必須同時將pk和id設為None:

django_blog.pk = None
django_blog.id = None
django_blog.save() # django_blog.pk == 4

對於外來鍵和多對多關係,更需要進一步處理。例如,Entry有一個ManyToManyField到Author。 複製條目後,您必須為新條目設定多對多關係,像下面這樣:

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

八、批量更新物件

使用update()方法可以批量為QuerySet中所有的物件進行更新操作。

# 更新所有2007年釋出的entry的headline
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

只可以對普通欄位和ForeignKey欄位使用這個方法。若要更新一個普通欄位,只需提供一個新的常數值。若要更新ForeignKey欄位,需設定新值為你想指向的新模型例項。例如:

>>> b = Blog.objects.get(pk=1)
# 修改所有的Entry,讓他們都屬於b
>>> Entry.objects.all().update(blog=b)

update方法會被立刻執行,並返回操作匹配到的行的數目(有可能不等於要更新的行的數量,因為有些行可能已經有這個新值了)。唯一的約束是:只能訪問一張資料庫表。你可以根據關係欄位進行過濾,但你只能更新模型主表的欄位。例如:

>>> b = Blog.objects.get(pk=1)
# Update all the headlines belonging to this Blog.
>>> Entry.objects.select_related().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表示式。這對於批量更新同一模型中某個欄位特別有用。例如增加Blog中每個Entry的pingback個數:

>>> Entry.objects.all().update(n_pingbacks=F('n_pingbacks') + 1)

然而,與filter和exclude子句中的F()物件不同,在update中你不可以使用F()物件進行跨表操作,你只可以引用正在更新的模型的欄位。如果你嘗試使用F()物件引入另外一張表的欄位,將丟擲FieldError異常:

# THIS WILL RAISE A FieldError
>>> Entry.objects.update(headline=F('blog__name'))

九、關係的物件

利用本節一開始的模型,一個Entry物件e可以通過blog屬性e.blog獲取關聯的Blog物件。反過來,Blog物件b可以通過entry_set屬性b.entry_set.all()訪問與它關聯的所有Entry物件。

1. 一對多(外來鍵)

正向查詢:

直接通過圓點加屬性,訪問外來鍵物件:

>>> e = Entry.objects.get(id=2)
>>> e.blog # 返回關聯的Blog物件

要注意的是,對外來鍵的修改,必須呼叫save方法進行儲存,例如:

>>> e = Entry.objects.get(id=2)
>>> e.blog = some_blog
>>> e.save()

如果一個外來鍵欄位設定有null=True屬性,那麼可以通過給該欄位賦值為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)  # 訪問資料庫,獲取實際資料
>>> print(e.blog)  # 不會訪問資料庫,直接使用快取的版本

請注意QuerySet的select_related()方法會遞迴地預填充所有的一對多關係到快取中。例如:

>>> e = Entry.objects.select_related().get(id=2)
>>> print(e.blog)  # 不會訪問資料庫,直接使用快取
>>> print(e.blog)  # 不會訪問資料庫,直接使用快取

反向查詢:

如果一個模型有ForeignKey,那麼該ForeignKey所指向的外來鍵模型的例項可以通過一個管理器進行反向查詢,返回源模型的所有例項。預設情況下,這個管理器的名字為FOO_set,其中FOO是源模型的小寫名稱。該管理器返回的查詢集可以用前面提到的方式進行過濾和操作。

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

使用自定義的反向管理器:

預設情況下,用於反向關聯的RelatedManager是該模型預設管理器的子類。如果你想為一個查詢指定一個不同的管理器,你可以使用下面的語法:

from django.db import models

class Entry(models.Model):
    #...
    objects = models.Manager()  # 預設管理器
    entries = EntryManager()    # 自定義管理器

b = Blog.objects.get(id=1)
b.entry_set(manager='entries').all()

當然,指定的自定義反向管理器也可以呼叫它的自定義方法:

b.entry_set(manager='entries').is_published()

處理關聯物件的其它方法:

除了在前面定義的QuerySet方法之外,ForeignKey管理器還有其它方法用於處理關聯的物件集合。下面是每個方法的概括。

add(obj1, obj2, ...):新增指定的模型物件到關聯的物件集中。

create(**kwargs):建立一個新的物件,將它儲存並放在關聯的物件集中。返回新建立的物件。

remove(obj1, obj2, ...):從關聯的物件集中刪除指定的模型物件。

clear():清空關聯的物件集。

set(objs):重置關聯的物件集。

若要一次性給關聯的物件集賦值,使用set()方法,並給它賦值一個可迭代的物件集合或者一個主鍵值的列表。例如:

b = Blog.objects.get(id=1)
b.entry_set.set([e1, e2])

在這個例子中,e1和e2可以是完整的Entry例項,也可以是整數的主鍵值。

如果clear()方法可用,那麼在將可迭代物件中的成員新增到集合中之前,將從entry_set中刪除所有已經存在的物件。如果clear()方法不可用,那麼將直接新增可迭代物件中的成員而不會刪除所有已存在的物件。

這節中的每個反向操作都將立即在資料庫內執行。所有的增加、建立和刪除操作也將立刻自動地儲存到資料庫內。

2. 多對多

多對多關係的兩端都會自動獲得訪問另一端的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.

與外來鍵欄位中一樣,在多對多的欄位中也可以指定related_name名。

(注:在一個模型中,如果存在多個外來鍵或多對多的關係指向同一個外部模型,必須給他們分別加上不同的related_name,用於反向查詢)

3. 一對一

一對一非常類似多對一關係,可以簡單的通過模型的屬性訪問關聯的模型。

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.

不同之處在於反向查詢的時候。一對一關係中的關聯模型同樣具有一個管理器物件,但是該管理器表示一個單一的物件而不是物件的集合:

e = Entry.objects.get(id=2)
e.entrydetail # 返回關聯的EntryDetail物件

如果沒有物件賦值給這個關係,Django將丟擲一個DoesNotExist異常。
可以給反向關聯進行賦值,方法和正向的關聯一樣:

e.entrydetail = ed

4. 反向關聯是如何實現的?

一些ORM框架需要你在關係的兩端都進行定義。Django的開發者認為這違反了DRY (Don’t Repeat Yourself)原則,所以在Django中你只需要在一端進行定義。

那麼這是怎麼實現的呢?因為在關聯的模型類沒有被載入之前,一個模型類根本不知道有哪些類和它關聯。

答案在app registry!在Django啟動的時候,它會匯入所有INSTALLED_APPS中的應用和每個應用中的模型模組。每建立一個新的模型時,Django會自動新增反向的關係到所有關聯的模型。如果關聯的模型還沒有匯入,Django將儲存關聯的記錄並在關聯的模型匯入時新增這些關係。

由於這個原因,將模型所在的應用都定義在INSTALLED_APPS的應用列表中就顯得特別重要。否則,反向關聯將不能正確工作。

5. 通過關聯物件進行查詢

涉及關聯物件的查詢與正常值的欄位查詢遵循同樣的規則。當你指定查詢需要匹配的值時,你可以使用一個物件例項或者物件的主鍵值。

例如,如果你有一個id=5的Blog物件b,下面的三個查詢將是完全一樣的:

Entry.objects.filter(blog=b) # 使用物件例項
Entry.objects.filter(blog=b.id) # 使用例項的id
Entry.objects.filter(blog=5) # 直接使用id

十、使用原生SQL語句

如果你發現需要編寫的Django查詢語句太複雜,你可以迴歸到手工編寫SQL語句。Django對於編寫原生的SQL查詢有許多選項。

最後,需要注意的是Django的資料庫層只是一個數據庫介面。你可以利用其它的工具、程式語言或資料庫框架來訪問資料庫,Django沒有強制指定你非要使用它的某個功能或模組。