1. 程式人生 > >part8:查詢操作

part8:查詢操作

web 例如 from 分別是 相對 foreign 對象 查詢語句 med

查詢操作是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.)

二、保存對象

>>> 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沒有強制指定你非要使用它的某個功能或模塊。

part8:查詢操作