django模型查詢操作的實現

一旦創建好瞭數據模型,Django就會自動為我們提供一個數據庫抽象API,允許創建、檢索、更新和刪除對象操作

下面的示例都是通過下面參考模型來對模型字段進行操作說明:

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(to=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

1、創建對象

Django使用更直觀的系統:模型類表示數據庫表,該類的實例表示數據庫表中的特定記錄也就是數據值

要創建對象,請使用模型類的關鍵字參數對其進行實例化,然後調用save()以將其保存到數據庫中,假設模型存在於mysite/app01/models.py文件中

from app01.models import Blog
b = Blog(name='beatles blog',tagline='all the latest beatles news.')
b.save()

它會在後臺執行SQL語句INSERT,如果不調用save()方法,Django不會立刻將該操作反應到數據庫中,save()方法沒有返回值,它可以接受一些額外的參數

#修改對象的值,在後臺會執行一條SQL語句的UPDATE
b.name = 'python is django'
b.save()
Blog.objects.all()
<QuerySet [<Blog: python is django>]>

如果想要一行代碼完成上面的操作,請使用create()方法,它可以省略save的步驟:

Blog.objects.create(name='Blog title',tagline='this is one blog title name')
Blog.objects.filter(name='Blog title')  #查詢數據
<QuerySet [<Blog: name:Blog title;tagline:this is one blog title name>]>

2、保存ForeignKey和ManyToManyField字段

保存一個ForeignKey字段和保存普通字段沒什麼區別,隻需要註意值的類型要正確,下面的例子,有一個Entry的實例和一個Blog的實例,然後把cheese_blog作為值賦給瞭entry的blog屬性,最後調用save方法進行保存。

from app01.models import Blog,Entry
entry = Entry.objects.get(pk=1)
cheese_blog = Blog.objects.get(name='Blog title')
entry.blog = cheese_blog
entry.save()

ManyToManyField字段的保存稍微有點區別,需要調用add()方法,而不是直接給屬性賦值,但它不需要調用save方法,如下示例:

from app01.models import Author
joe = Author.objects.create(name='Joe')
entry.authors.add(joe)

在對對多關系中可以一次性添加多條記錄,隻需在調用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)

 
#如指定瞭錯誤的類型對象,將引發異常

3、檢索對象

想要從數據庫中檢索對象,需要基於模型類,通過管理器(Manager)構造一個查詢結果集(QuerySet),每個QuerySet代表一些數據庫對象的集合,它可以包含零個、一個或多個過濾器(filters),Filters縮小查詢結果的范圍,在SQL語法中,一個QuerySet相當於一個SELECT語句,而filter相當於WHERE或者LIMIT一類的子句

通過模型的Manager獲得QuerySet,每個模型至少具有一個Manager,默認情況下,它被稱作為objects,可以通過模型類直接調用它,但不能通過模型類的實例調用它,以此實現‘表級別’操作和‘記錄級別’操作的強制分離,如下所示:

Blog.objects
<django.db.models.manager.Manager object at 0x000001E3583EFDD8>
b = Blog(name='FOO',tagline='Bar')
b.objects
Traceback (most recent call last):
......
AttributeError: Manager isn't accessible via Blog instances

 Managers隻能通過模型類訪問,不能通過模型實例訪問,它強制表級操作和記錄操作之間的分離

#檢索所有對象,可使用all()方法,可獲取某張表的所有記錄
alll_entries = Entry.objects.all()

#過濾對象,有兩種方法用來過濾QuerySet的結果:
filter(**kwargs) :返回一個根據指定參數查詢出來的QuerySet
exclude(**kwargs) :返回給定查找參數不匹配的QuerySet
#例如:獲取2006年的博客條目,使用filter()
Entery.objects.filter(pub_date__year=2006)
#使用默認的manager類,它等同於上
Entery.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.date(2005.1.30))

#它將獲取數據庫中所有條目中帶'what'的首字母的QuerySet,然後排除,添加過濾器然後過濾是2005年1月30日的條目
被過濾的QuerySets都是唯一的,每次過濾,都會獲得一個全新的QuerySet,它和之前的QuerySet沒有任何關系,可以完全獨立的被保存,使用和重用

QuerySets是懶惰的,一個創建QuerySet的動作不會立刻導致任何數據庫的行為,你可以不斷的進行filter動作,Django不會運行任何實際的數據庫查詢操作,直到QuerySet被提交(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語句時才執行數據庫操作,通常QuerySet的檢索不會立刻執行實際的數據庫操作,知道出現類似print的請求,也就是evaluated

檢索單個對象:filter方法始終返回的是QuerySet,哪怕隻有一個對象符合顧慮條件,返回的也是包含一個對象的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方法要慎用!

其他QuerySet方法:大多數情況下,需要從數據庫中查找對象時,使用all()、get()、filter()和exclude()就行,針對QuerySet的方法還有很多高級用法可以參考官網

QuerySet使用限制:使用類似於python對列表進行切片的方法可以對QuerySet進行范圍取值,它相當於SQL語句中的LIMIT和OFFSET子句

Entry.objects.all()[:5]    #返回前5個對象
Entry.objects.all()[5:10]  #返回第6個到第10個對象

註意:QuerySet不支持負索引,如:Entry.objects.all()[-1]是不允許的

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

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

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

Entry.objects.order_by('headline')[0]
#相當於:
Entry.objects.order_by('headline')[0:1].get()

#註意:如果沒有匹配到對象,第一種方法會拋出IndexError異常,而第二種
#方法會拋出DoesNotExist異常,所以在使用get和切片時,需要註意查詢結果的元素個數

字段查詢:字段查詢是指SQL WHERE子句內容的方式,也就是filter()、exclude()和get()等方法的關鍵字參數,其基本格式是:field__lookuptype=value(註意是雙下劃線)

#pub_date為字段名鏈接雙下劃線再指定查找類型的關鍵字參數
Entry.objects.filter(pub_date__lte='2006-01-01')
#相當於SQL語句:
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

其中的字段必須是模型種定義的字段之一,但是有一個例外,那就是ForeignKey字段,你可以為其添加一個“_id“後綴(單下劃線),這種情況下鍵值是外鍵模型的主鍵原始值,如:

#blog為外鍵模型
Entry.objects.filter(blog_id=4)
#如果你傳遞瞭一個非法的鍵值,查詢函數會拋出TypeError異常

Django的數據庫API支持20多種查詢類型,下面介紹一些常用的:

#exact為模型類型,如果不提供查詢類型,那查詢類型就是這個默認的exact,精確匹配
Entry.objects.get(headline__exact="Cat bites dog")
#SQL語句:
SELECT ... WHERE headline='Cat bites dog';
#下面兩個相當:
Blog.objects.get(id__exact=4)
Blog.objects.get(id=4)

#iexact:不區分大小寫的完全匹配
Blog.objects.get(name__iexact='beatles blog')
SELECT ... WHERE name ILIKE 'beatles blog'

#contains:區分大小寫
Entry.objects.get(headline__contains='Lennon')
SELECT ... WHERE headline LIKE '%Lennon%';

#icontains:不區分大小寫
Entry.objects.get(headline__icontains='Lennon')
SELECT ... WHERE headline ILIKE '%Lennon%';

#in:包含在列表、元組或集合的,也可接受字符串(可迭代)
Entry.objects.filter(id__in=[1,3,4])

#gt:大於
Entry.objects.filter(id__gt=3)

#gte:大於或等於
Entry.objects.filter(id__gte=3)

#lt:小於
Entry.objects.filter(id__lt=3)

#lte:小於或等於
Entry.objects.filter(id__lte=3)

#startswith:區分大小寫的開頭
Entry.objects.filter(headline__startswith='Lennon')

#istartswith:不區分大小寫的開頭
Entry.objects.filter(headline__istartswith='Lennon')

#endswith:區分大小寫的結尾;iendswith:不區分大小寫的結尾
Entry.objects.filter(headline__endswith='Lennon')
Entry.objects.filter(headline__iendswith='Lennon')

#range:范圍在內
import datetime
start_date = datetime.date(2005,1,1)
end_date = datetime.date(2005,12,31)
Entry.objects.filter(pub_date__range=(start_date,end_date))

#date:對於datetime字段,將值轉換為日期,采用日期值查找
Entry.objects.filter(pub_date__date=datetime.date(2005,1,1))
Entry.objects.filter(pub_date__date__gt=datetime.date(2005,1,1))

#year:對於日期和時間字段,確切的年份匹配,需要整數年
Entry.objects.filter(pub_date__year=2005)
Entry.objects.filter(pub_date__year__gte=2005)

#iso_year:精確的ISO 8601周編年份匹配,需要整數年
Entry.objects.filter(pub_date__iso_year=2005)
Entry.objects.filter(pub_date__iso_year__gte=2005)

#month:日期和日期時間字段,確切的月份匹配
Entry.objects.filter(pub_date__month=1)
Entry.objects.filter(pub_date__month__gte=5)

#day:確切的天匹配
Entry.objects.filter(pub_date__day=3)
Entry.objects.filter(pub_date__day__gte=3)

#week:日期和時間字段,根據ISO-8601返回周數
Entry.objects.filter(pub_date__week=52)
Entry.objects.filter(pub_date__week__gte=32,pub_date__week__lte=38)

#week_day:匹配星期幾,從1(星期日)到7(星期六)的整數值
Entry.objects.filter(pub_date__week_day=2)
Entry.objects.filter(pub_date__week_day__gte=2)

#quarter:匹配四季,取1到4之間的整數值
Entry.objects.filter(pub_date__quarter=2)

#time:對於datetime字段,將值轉換為時間,取一個datetime.time值
Entry.objects.filter(pub_date__time=datetime.time(14,30))

#hour:對日期時間和時間字段,精確的小時匹配,取值0-23
Entry.objects.filter(timestamp__hour=11)

#minute:對於日期時間和時間字段,精確分鐘匹配
Entry.objects.filter(timestamp__minute=29)

#second:對於日期時間和時間字段,確切的妙匹配
Entry.objects.filter(timestamp__second=31)

#isnull:采用任一True或False,其對應於SQL查詢IS NULLIS NOT NULL
Entry.objects.filter(pub_date__isnull=True)
SELECT ... WHERE pub_date IS NULL

#regex:區分大小寫的正則表達式匹配,語法為python re模塊語法
Entry.objects.filter(headline__regex=r'^(an?|The)+')

#iregex:不區分大小寫的正則表達式匹配
Entry.objects.filter(headline__iregex=r'^(an?|the)+')

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

#先指定字段名然後雙下劃線分隔指定外鍵字段名
Entry.objects.filter(blog__name='Blog title')

如果要引用一個反向關聯隻需要使用模型的小寫名即可:

#獲取所有的blog對象,但前提是所關聯的Entry模型的headline字段包含'django models'
Blog.objects.filter(entry__headline__icontains='django models')

如果多級關聯種進行過濾而且其中某個模型沒有滿足過濾條件的值,Django將把它當作一個空(null)但是合法的對象,不會拋出任何異常或錯誤:

#查詢Blog的所有對象,但關聯entry模型種authors字段關聯名必須時py.qi
Blog.objects.filter(entry__authors__name='py.qi')

如果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='django models',entry__pub_date__year=2019)

這是一個跨外鍵、兩個過濾參數的查詢,此時我們理解兩個參數之間屬於‘and’的關系,也就是說,過濾出來的Blog對象對用的entry對象必須滿足上面兩個條件,但是,我們看下面的用法:

Blog.objects.filter(entry__headline__contains='django models').filter(entry__pub_date__year=2019)

把兩個參數拆分,放在兩個filer調用裡面,安裝我們前面說過的鏈式過濾,這裡的結果和上面的例子應該一樣,可實際上,它不一樣,Djang在這種情況下,將兩個filter之間的關系設計為’or’,多對對關系下的多值查詢和外鍵foreignkey的情況一樣。

但是,exclude的策略設計又和filter不一樣:

Blog.objects.exclude(entry__headline__contains='django models',entry__pub_date__year=2019)

它會排除headline中包含‘django models’的entry或在2019年發佈的Entry,中間是一個’or’的關系

那要排除同時滿足上面兩個條件的對象,應該這麼做:

Blog.objects.exclude(entry__in=Entry.objects.filter(headline__contains='django models',pub_date__year=2019,),)

使用F表達式引用模型的字段:

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

使用Django提供的F表達式;例如:為瞭查找commnets數目多於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比pinggback和comments數目綜合還要小的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和datetime字段,還可以加或減去一個timedelta對象,下面的例子將返回發佈時間超過3天後別修改的所有Entry:

Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

F()對象還支持:.bitand()、.bitor()、.bitrightshift()和.bitleftshift()4種位操作:

F('somefield').bitand(16)

主鍵的快捷查詢方式:pk

pk就是primary key的縮寫,通常情況下,一個模型的主鍵位’id’,所以下面三個語句的效果一樣:

Blog.objects.get(id__exact=1)
Blog.objects.get(id=1)
Blog.objects.get(pk=1)

可以聯合其他類型的參數:

Blog.objects.filter(pk__in=[1,3,4])
Blog.objects.filter(pk__gt=10)

可以跨表操作:

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

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

Entry.objects.filter(headline__contains='%')
#SQL語句
SELECT ... WHERE headline LIKE '%\%%';

4、緩存和查詢集

每個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[2]) #再次查詢數據庫

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

queryset = Entry.objects.all()
[entry for entry in queryset]   #查詢數據庫
print(queryset[3])   #使用緩存
print(queryset[1])   #使用緩存

下面的操作都將遍歷QuerySet並建立緩存:

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

註意:簡單的打印QuerySet並不會建立緩存,因為__repr__()調用隻返回全部查詢集的一個切片

5、使用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關系:

from app01.models import Entry
from django.db.models import Q
from datetime import date
Entry.objects.get(Q(headline__startswith='a'),Q(pub_date=date(2005,1,1)) | Q(pub_date=date(2019,1,1)))

#它相當於SQL的
SELECT * FROM Entry WHERE headline LIKE 'a%' AND (pub_date =  '2005,1,1' OR pub_date = '2019,1,1)

當關鍵字參數和Q對象組合使用時,Q對象必須放在前面,否則會報錯

Entry.objects.filter(Q(pub_date=date(2005,4,2)) | Q(pub_date=date(2019,4,2)),headline__startswith='a')

#如果將headline__startswith='a'放在前面將會報錯

6、比較對象

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

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

如果模型的主鍵不叫’ID’也沒關系,後臺總是會使用正確的主鍵名字進行比較,如下:主鍵名為’name‘時,下面的方法一樣

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

7、刪除對象

刪除對象使用的是對象的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()

8、復制模型實例

雖然沒有內置的方法用於復制模型的實例,但還是很容易創建一個新的實例並將原實例的所有字段都拷貝過來,最簡單的方法是將原實例的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()

9、批量更新對象

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

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

隻可以對普通字段和ForignKey字段使用這個方法,若要更新一個普通字段,隻需提供一個新的常數值,若要更新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_save或post_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'))

10、關系的對象

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

1)一對多外鍵

正向查詢:

直接通過圓點加屬性,訪問外鍵對象:

e = Entry.objects.get(id=2)
e.blog

要註意的是,對外鍵的修改,必須調用save方法進行保存

e = Entry.objects.get(id=2)
other_blog = Blog.objects.get(name='django')
e.blog = other_blog
e.save()

如果一個外鍵字段設置有Null=True屬性,那麼可以通過給該字段賦值為None的方法移除外鍵值:

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

在第一次對一個外鍵關系進行正向訪問的時候,關系對象會被緩存,隨後對同樣外鍵關系對象的訪問會使用這個緩存,如:

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=7)  
b.entry_set.all()  #Returns all Entry objects related to Blog
b.entry_set.filter(headline__contains='Every')  #b.entries is a Manager that returns QuerySets
b.entry_set.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的工作方式與前面提到的‘反向’一對多關系的用法一樣,唯一的區別在於屬性的名稱,定義ManyToMangField的模型使用該字段的屬性名稱,而‘反向’模型使用源模型的小寫名稱加上‘_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

11、使用原生SQL語句

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

最後需要註意的是Django的數據庫層隻是一個數據庫接口,你可以利用其它的工具,編程語言或數據庫框架來訪問數據庫,django沒有強制指定你非要使用它的某個功能或模塊。

到此這篇關於django模型查詢操作的文章就介紹到這瞭,更多相關django模型查詢操作內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: