1. 程式人生 > >Django ORM之QuerySet詳解

Django ORM之QuerySet詳解

##概述:
Django ORM用到三個類:Manager、QuerySet、Model。Manager定義表級方法(表級方法就是影響一條或多條記錄的方法),我們可以以models.Manager為父類,定義自己的manager,增加表級方法;QuerySet:Manager類的一些方法會返回QuerySet例項,QuerySet是一個可遍歷結構,包含一個或多個元素,每個元素都是一個Model 例項,它裡面的方法也是表級方法,前面說了,Django給我們提供了增加表級方法的途徑,那就是自定義manager類,而不是自定義QuerySet類,一般的我們沒有自定義QuerySet類的必要;django.db.models模組中的Model類,我們定義表的model時,就是繼承它,它的功能很強大,通過自定義model的instance可以獲取外來鍵實體等,它的方法都是記錄級方法(都是例項方法,貌似無類方法),不要在裡面定義類方法,比如計算記錄的總數,檢視所有記錄,這些應該放在自定義的manager類中

一、QuerySet簡介

每個Model都有一個預設的manager例項,名為objects,QuerySet有兩種來源:通過manager的方法得到、通過QuerySet的方法得到。mananger的方法和QuerySet的方法大部分同名,同意思,如filter(),update()等,但也有些不同,如manager有create()、get_or_create(),而QuerySet有delete()等,看原始碼就可以很容易的清楚Manager類與Queryset類的關係,Manager類的絕大部分方法是基於Queryset的。一個QuerySet包含一個或多個model instance。QuerySet類似於Python中的list,list的一些方法QuerySet也有,比如切片,遍歷。

>>> from userex.models import UserEx
>>> type(UserEx.objects)
<class ‘django.db.models.manager.Manager’>

>>> a = UserEx.objects.all()
>>> type(a)
<class ‘django.db.models.query.QuerySet’>

QuerySet是延遲獲取的,只有當用到這個QuerySet時,才會查詢資料庫求值。另外,查詢到的QuerySet又是快取的,當再次使用同一個QuerySet時,並不會再查詢資料庫,而是直接從快取獲取(不過,有一些特殊情況)。一般而言,當對一個沒有求值的QuerySet進行運算,返回的是QuerySet、ValuesQuerySet、ValuesListQuerySet、Model例項時,一般不會立即查詢資料庫;反之,當返回的不是這些型別時,會查詢資料庫。下面介紹幾種(並非全部)對QuerySet求值的場景。

class Blog(models.Model):

    name = models.CharField(max_length=100)

    tagline = models.TextField()


    def __unicode__(self):

        return self.name


class Author(models.Model):

    name = models.CharField(max_length=50)

    email = models.EmailField()

 

    def __unicode__(self):

        return self.name

 

class Entry(models.Model):

    blog = models.ForeignKey(Blog)

    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 __unicode__(self):

        return self.headlin

我們就以上面的models為例。

1.遍歷

a = Entry.objects.all()
for e in a:
    print (e.headline)

當遍歷一開始時,先從資料庫執行查詢select * from Entry得到a,然後再遍歷a。注意:這裡只是查詢Entry表,返回的a的每條記錄只包含Entry表的欄位值,不管Entry的model中是否有onetoone、onetomany、manytomany欄位,都不會關聯查詢。這遵循的是資料庫最少讀寫原則。我們修改一下程式碼,如下,遍歷一開始也是先執行查詢得到a,但當執行print (e.blog.name)時,還需要再次查詢資料庫獲取blog實體。

from django.db import connection

l = connection.queries  #l是一個列表,記錄SQL語句
a = Entry.objects.all()

for e in a:
    print (e.blog.name)
    print(len(l))
    

遍歷時,每次都要查詢資料庫,l長度每次增1,Django提供了方法可以在查詢時返回關聯表實體,如果是onetoone或onetomany,那用select_related,不過對於onetomany,只能在主表(定義onetomany關係的那個表)的manager中使用select_related方法,即通過select_related獲取的關聯物件是model instance,而不能是QuerySet,如下,e.blog就是model instance。對於onetomany的反向和manytomany,要用prefetch_related,它返回的是多條關聯記錄,是QuerySet。

a = Entry.objects.select_related('blog')
for e in a:
    print (e.blog.name)
    print(len(l))

可以看到從開始到結束,l的長度只增加1。另外,通過查詢connection.queries[-1]可以看到Sql語句用了join。所以使用select_related來獲取查詢關聯物件,速度會更快。

2.切片

切片不會立即執行,除非顯示指定了步長,如a= Entry.objects.all()[0:10:2],步長為2。

3.序列化,即Pickling

序列化QuerySet很少用,這裡不討論,讀者可以自行查詢資料。

4.repr()

和str()功能相似,將物件轉為字串,很少用。

5.len()

計算QuerySet元素的數量,並不推薦使用len(),除非QuerySet是求過值的(即evaluated),否則,用QuerySet.count()獲取元素數量,這個效率要高。

6.list()

將QuerySet轉為list

7.bool(),判斷是否為空

if Entry.objects.filter(headline="Test"):
     print("There is at least one Entry with the headline Test")

同樣不建議這種方法判斷是否為空,而應該使用QuerySet.exists(),查詢效率高。

二、 QuerySet的方法

資料庫的常用操作就四種:增、刪、改、查,QuerySet的方法涉及刪、改、查。後面還會講model物件的方法,model方法主要是增、刪、改、還有呼叫model例項的欄位。

1.delete():刪

原型:delete()
返回:None
相當於sql語句的delete-from-where, delete-from-join-where。先filter,然後對得到的QuerySet執行delete()方法就行了,它會同時刪除關聯它的那些記錄,比如我刪除記錄表1中的A記錄,表2中的B記錄中有A的外來鍵,那同時也會刪除B記錄,那ManyToMany關係呢?對於ManyToMany,刪除其中一方的記錄時,會同時刪除中間表的記錄,即刪除雙方的關聯關係。由於有些資料庫,如Sqlite不支援delete與limit連用,所以在這些資料庫對QuerySet的切片執行delete()會出錯。如

>>> a = UserEx.objects.filter(is_active=False)

>>> b = a[:3]

>>> b.delete() #執行時會報錯

解決:UserEx.objects.filter(pk__in=b).delete() ,in後面可以是一個QuerySet。

2.update():改

批量修改,返回修改的記錄數。不過update()中的鍵值對的鍵只能是主表中的欄位,不能是關聯表字段,如下

Entry.objects.update(blog__name='foo')  #錯誤,無法修改關聯表字段,只能修改Entry表的欄位

Entry.objects.filter(blog__name='foo').update(comments_on=False)  #正確

最好的方法是先filter,查詢出QuerySet,然後再執行QuerySet.update()。

3.filter(**kwargs)、exclude(**kwargs)、get(**kwargs):查詢

相當於select-from-where,select-from-join-where,很多網站讀資料庫操作最多。可以看到,filter()的引數是變個數的鍵值對,而不會出現>,<,!=等符號,這些符號分別用__gt,__lt,~Q或exclude(),不過對於!=,建議使用Q查詢,更不容易出錯。可以使用雙下劃線對OneToOne、OneToMany、ManyToMany進行關聯查詢和反向關聯查詢,而且方法都是一樣的,如:

>>> Entry.objects.filter(blog__name='Beatles Blog') #限定外來鍵表的欄位

下面是反向連線,不過要注意,這裡不是entry_set,entry_set是Blog instance的一個屬性,代表某個Blog object的關聯的所有entry,而QuerySet的方法中反向連線是直接用model的小寫,不要把兩者搞混。

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

>>> Blog.objects.filter(entry__authors__name='Lennon')   #ManyToMany關係,反向連線

>>> myblog = Blog.objects.get(id=1)

>>> Entry.objects.filter(blog=myblog) #正向連線。既可以用實體,也可以用實體的主鍵,其實即使用實體,也是隻用實體的主鍵而已。這兩種方式對OneToOne、OneToMany、ManyToMany的正向、反向連線都適用。


>>> Entry.objects.filter(blog=1)    #我個人不建議這樣用,對於create(),不支援這種用法

>>> myentry = Entry.objects.get(id=1)

>>> Blog.objects.filter(entry=myentry) #ManyToMany反向連線。與下面兩種方法等價

>>> Blog.objects.filter(entry=1)   

>>> Blog.objects.filter(entry_id=1)  #適用於OneToOne和OneToMany的正向連線

OneToOne的關係也是這樣關聯查詢,可以看到,Django對OneToOne、OneToMany、ManyToMany關聯查詢及其反向關聯查詢提供了相同的方式,真是牛逼啊。對於OneToOne、OneToMany的主表,也可以使用下面的方式:
Entry.objects.filter(blog_id=1),因為blog_id是資料庫表Entry的一個欄位, 這條語句與Entry.objects.filter(blog=blog1)生成的SQL是完全相同的。
與filter類似的還有exclude(**kwargs)方法,這個方法是剔除,相當於sql語句的select-from-where not。可以使用雙下劃線對OneToOne、OneToMany、ManyToMany進行關聯查詢和反向關聯查詢,方法與filter()中的使用方法相同。

>>> Entry.objects.exclude(pub_date__gt=datetime.date(2005, 1, 3), headline='Hello')

轉為SQL為:

SELECT *

FROM Entry

WHERE NOT (pub_date > '2005-1-3' AND headline = 'Hello')

4.SQL其它關鍵字在django中的實現

在SQL中,很多關鍵詞在刪、改、查時都是可以用的,如order by、 like、in、join、union、and、or、not等等,我們以查詢為例,說一下django如何對映SQL的這些關鍵字的(查、刪、改中這些關鍵字的使用方法基本相同)。

1.F類(無對應SQL關鍵字)

前面提到的filter/exclude中的查詢引數值都是常量,如果我們想比較model的兩個欄位怎麼辦呢?Django也提供了方法,F類,F類例項化時,引數也可以用雙下劃線,也可以邏輯運算,如下:

>>> from django.db.models import F

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

>>> from datetime import timedelta

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

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

2.Q類(對應and/or/not)

如果有or等邏輯關係呢,那就用Q類,filter中的條件可以是Q物件與非Q查詢混和使用,但不建議這樣做,因為混和查詢時Q物件要放前面,這樣就有難免忘記順序而出錯,所以如果使用Q物件,那就全部用Q物件。Q物件也很簡單,就是把原來filter中的各個條件分別放在一個Q()即可,不過我們還可以使用或與非,分別對應符號為”|”和”&”和”~”,而且這些邏輯操作返回的還是一個Q物件,另外,逗號是各組條件的基本連線符,也是與的關係,其實可以用&代替(在python manage.py shell測試過,&代替逗號,執行的SQL是一樣的),不過那樣的話可讀性會很差,這與我們直接寫SQL時,各組條件and時用換行一樣,邏輯清晰。

from django.db.models import Q

>>> Poll.objects.get( Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),question__startswith='Who')   #正確,但不要這樣混用

>>> Poll.objects.get( Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),Q(question__startswith='Who'))  #推薦,全部是Q物件

>>> Poll.objects.get( (Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)))&Q(question__startswith='Who'))  #與上面語句同意,&代替”,”,可讀性差

Q類中時應該可以用F類,待測試。

3.annotate(無對應SQL關鍵字)

函式原型annotate(*args, **kwargs)、
返回QuerySet
往每個QuerySet的model instance中加入一個或多個欄位,欄位值只能是聚合函式,因為使用annotate時,會用group by,所以只能用聚合函式。聚合函式可以像filter那樣關聯表,即在聚合函式中,Django對OneToOne、OneToMany、ManyToMany關聯查詢及其反向關聯提供了相同的方式,見下面例子。

#計算每個使用者的userjob數量,欄位命名為ut_num,返回的QuerySet中的每個object都有
#這個欄位。在UserJob中定義User為外來鍵,在Job中定義與User是ManyToMany
>>> from django.contrib.auth.models import User
>>> from django.db.models import Count

>>>> a = User.objects.filter(is_active=True, userjob__is_active=True). annotate(n=Count(‘userjob’)) #一對多反向連線
>>> b = User.objects.filter(is_active=True, job__is_active=True).annotate(n=Count(‘job__name’))  #多對多反向連線,User與Job是多對多
>>> len(a)  #這裡才會對a求值
>>> len(b)  #這裡才會對b求值

a對應的SQL語句為(SQL中沒有為表起別名,u、ut是我加的):

select auth.user.*,Count(ut.id) as ut_num
from auth_user as u
left outer join job_userjob as ut on u.id = ut.user_id
where u.is_active=True and ut.is_active=True
group by u.*

4.order_by——對應order by

函式原型 order_by(*fields)
返回QuerySet
正向的反向關聯表跟filter的方式一樣。如果直接用欄位名,那就是升序asc排列;如果欄位名前加-,就是降序desc。

Entry.objects.all().order_by("pub_date ")

5.distinct——對應distinct

原型 distinct()
一般與values()、values_list()連用,這時它返回ValuesQuerySet、ValuesListQuerySet
這個類跟列表很相似,它的每個元素是一個字典。它沒有引數(其實是有引數的,不過,引數只在PostgreSQL上起作用)。使用方法為:

>>> a=Author.objects.values_list(name).distinct()
>>> b=Author.objects.values_list(name,email).distinct()

對應的SQL分別為

select distinct name
from Author
和
seect distinct name,email
from Author

6.values()和values_list()——對應‘select 某幾個欄位’

函式原型values(*field), values_list(*field)
返回ValuesQuerySet, ValuesListQuerySet

Author.objects.filter(**kwargs)對應的SQL只返回主表(即Author表)的所有欄位值,即使在查詢時關聯了其它表,關聯表的欄位也不會返回,只有當我們通過Author instance用關聯表時,Django才會再次查詢資料庫獲取值。當我們不用Author instance的方法,且只想返回幾個欄位時,就要用values(),它返回的是一個ValuesQuerySet物件,它類似於一個列表,不過,它的每個元素是字典。而values_list()跟values()相似,它返回的是一個ValuesListQuerySet,也型別於一個列表,不過它的元素不是字典,而是元組。一般的,當我們不需要model instance的方法且返回多個欄位時,用values(*field),而返回單個欄位時用values_list(‘field’,flat=True),這裡flat=True是要求每個元素不是元組,而是單個值,見下面例子。而且我們可以返回關聯表的欄位,用法跟filter中關聯表的方式完全相同。

>>> a = User.objects.values(‘id’,’username’,’userex__age’)
>>> type(a)
<class ‘django.db.models.query.ValuesQuerySet’>
>>> a
[{‘id’:0,’username’:u’test0’,’ userex__age’: 20},{‘id’:1,’username’:u’test1’,’userex__age’: 25},{‘id’:2,’username’:u’test2’, ’ userex__age’: 28}]
>>> b= User.objects.values_list(’username’,flat=True)
>>> b
[u’test0’, u’test1’ ,u’test2’]

7.select_related()——對應返回關聯記錄實體

原型select_related(*filed)
返回QuerySet

它可以指定返回哪些關聯表model instance,這裡的field跟filter()中的鍵一樣,可以用雙下劃線,但也有不同,QuerySet中的元素中的OneToOne關聯及外來鍵對應的是都是關聯表的一條記錄,如my_entry=Entry.objects.get(id=1),my_entry.blog就是關聯表的一條記錄的物件。select_related()不能用於OneToMany的反向連線,和ManyToMany,這些都是model的一條記錄對應關聯表中的多條記錄。前面提到了對於a = Author.objects.filter(**kwargs)這類語句,對應的SQL只返回主表,即Author的所有欄位,並不會返回關聯表字段值,只有當我們使用關聯表時才會再查資料庫返回,但有些時候這樣做並不好。看下面兩段程式碼,這兩段程式碼在1.1中提到過。在程式碼1中,在遍歷a前,先執行a對應的SQL,拿到資料後,然後再遍歷a,而遍歷過程中,每次都還要查詢資料庫獲取關聯表。程式碼2中,當遍歷開始前,先拿到Entry的QuerySet,並且也拿到這個QuerySet的每個object中的blog物件,這樣遍歷過程中,就不用再查詢資料庫了,這樣就減少了資料庫讀次數。

a = Entry.objects.all()
for e in a:
    print (e.blog.name)
################  
a = Entry.objects.select_related('blog')
for e in a:
    print (e.blog.name)

8.prefetch_related(*field) ——對應返回關聯記錄實體的集合

函式原型prefetch_related(*field)
返回的是QuerySet

這裡的field跟filter()中的鍵一樣,可以用雙下劃線。用於OneToMany的反向連線,及ManyToMany。其實,prefetch_related()也能做select_related()的事情,但由於策略不同,可能相比select_related()要低效一些,所以建議還是各管各擅長的。select_related是用select ……join來返回關聯的表字段,而prefetch_related是用多條SQL語句的形式查詢,一般,後一條語句用IN來呼叫上一句話返回的結果。

class Restaurant(models.Model):
    pizzas = models.ManyToMany(Pizza, related_name='restaurants')
    best_pizza = models.ForeignKey(Pizza, related_name='championed_by')

>>> Restaurant.objects.prefetch_related('pizzas__toppings')
>>> Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')

先用select_related查到best_pizza物件,再用prefetch_related 從best_pizza查出toppings

9.extra()——實現複雜的where子句

具體可以看這篇文章:https://www.cnblogs.com/gaoya666/p/8877116.html

10. aggregate(*args, **kwargs)——對應聚合函式

引數為聚合函式,最好用**kwargs的形式,每個引數起一個名字。
該函式與annotate()有何區別呢?annotate相當於aggregate()和group by的結合,對每個group執行aggregate()函式。而單獨的aggregate()並沒有group by。

>>> from django.db.models import Count
>>> q = Blog.objects.aggregate(Count('entry'))  #這是用*args的形式,最好不要這樣用
>>> q = Blog.objects.aggregate(number_of_entries=Count('entry'))  #這是用**kwargs的形式
{'number_of_entries': 16}

至此,我們總結了QuerySet方法返回的資料形式,主要有五種。第一種:返回QuerySet,每個object只包含主表字段;第二種:返回QuerySet,每個object除了包含主表所有欄位,還包含某些關聯表的object,這種情況要用select_related()和prefetch_related(),可以是任意深度(即任意多個雙下劃線)的關聯,通常一層關聯和二層關聯用的比較多;第三種:返回ValuesQuerySet, ValuesListQuerySet,它們的每個元素包含若干主表和關聯表的欄位,不包含任何實體和關聯例項,這種情況要用values()和values_list();第四種:返回model instance;第五種:單個值,如aggregate()方法。

11.exists()、count()、len()

如果只是想知道一個QuerySet是否為空,而不想獲取QuerySet中的每個元素,那就用exists(),它要比len()、count()、和直接進行if判斷效率高。如果只想知道一個QuerySet有多大,而不想獲取QuerySet中的每個元素,那就用count();如果已經從資料庫獲取到了QuerySet,那就用len()。

12.contains/startswith/endswith——對應like

欄位名加雙下劃線,除了它,還有icontains,即Case-insensitive contains,這個是大小寫不敏感的,這需要相應資料庫的支援。有些資料庫需要設定才能支援大小寫敏感。

13. in——對應in

使用:欄位名加雙下劃線

14.exclude(field__in=iterable)——對應not in

iterable是可迭代物件

15.gt/gte/lt/lte——對應於>,>=,<,<=

欄位名加雙下劃線

16.range——對應於between and

欄位名加雙下劃線,range後面值是列表

17.isnull——對應於is null

Entry.objects.filter(pub_date__isnull=True)對應的SQL為SELECT … WHERE pub_date IS NULL;

18. QuerySet切片——對應於limit

QuerySet的索引只能是非負整數,不支援負整數,所以QuerySet[-1]錯誤

a=Entry.objects.all()[5:10]
b=len(a) 

執行Entry.objects.all()[5:8],對於不同的資料庫,SQL語句不同,Sqlite 的SQL語句為select * from tablename limit 3 offset 5; MySQL的SQL語句為select * from tablename limit 5,3

原文連結:https://www.cnblogs.com/ajianbeyourself/p/3604332.html#_label1