Skip to content

Instantly share code, notes, and snippets.

@hdyen
Last active August 29, 2015 14:15
Show Gist options
  • Save hdyen/1df0f9ea4ae75d16a48e to your computer and use it in GitHub Desktop.
Save hdyen/1df0f9ea4ae75d16a48e to your computer and use it in GitHub Desktop.

Making queries

原文:Making queries

參考:data model reference

之後的例子,都是用下面的資料模型

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

    def __str__(self):              # __unicode__ on Python 2
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog, related_name='entries')
    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

建立物件

model class → database table model instance → table record

建立物件流程:

  1. 初始化一個 model instance,並依需求附加關鍵字參數 (keyword arguments)。
  2. 呼叫 instance 的 save() 方法。
>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

這個流程的背面提供了 INSERT 的 SQL 指令。直到呼叫 save(),Django 才真正去對資料庫做操作。 save() 方法沒有回傳值。

參考:save()

要同時建立並儲存物件,使用 create() 方法:

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

儲存變更到物件中

要儲存對已存在資料庫的物件所做的修改,一樣使用 save()

b = Blog.objects.get(id=1)
b.name = 'New name'
b.save()

這裡的 save() 提供了 SQL 的 UPDATE 命令。同時的,直到呼叫 save(),Django 才會對資料庫進行操作。

儲存外鍵和多對多欄位

更新外鍵欄位的機制跟儲存一般欄位的方法一樣:指定合適的資料物件到欄位中。

>>> 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() 方法來增加 record 到關係中。

>>> from blog.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)

取出物件

為了取出資料庫中的物件,要透過資料模型類別上的 Manager 建立一個 QuerySet

一個查詢集代表資料庫中部分物件的集合。它可以有零至多個過濾器。過濾器能根據提供的參數,縮小查詢的結果。以 SQL 來看,一個查詢集等於一個 SELECT 命令,而過濾器等同於 SQL 的限制句,如 WHERELIMIT

使用資料模型別類的 Manager 來獲得查詢集。每個 model 至少有一個 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 只能透過 model class 存取,不能透過 model instance。這樣能將資料表層次 (table-level) 和記錄層次 (record-level) 的操作分開。

取出所有物件

使用 Managerall() 方法 來取出所有物件:

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

使用過濾器取出指定的物件集合

使用 all() 回傳的是整個集合,使用過濾器回傳的則是子集。

為了建立子集,透過加入過濾器條件,來縮小 (refine) 初始的 QuerySet

最常用來重定義查詢集的方法有兩種:

filter(**kwargs)

回傳新的查詢集,集合中的物件符合給定的查詢參數。

exclude(**kwargs)

回傳不符合查詢參數的的查詢集。

查詢參數 **kwargs 的格式需符合 Field lookups 裡所指。

取得 2006 年部落格文章的查詢集:

Entry.objects.filter(pub_date__year=2006)

等價於

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

串接 filter

縮小查詢集的結果也是查詢集,所以可以將細化的過程接起來:

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

最終結果的查詢集包含所有 headline 起頭是 'What',發佈日期介於 2005/01/30 至今天的文章。

Filtered QuerySets are unique

每次對一個查詢集做細化所得的新查詢集都是新的,沒有跟舊的綁在一起。

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

q2q3 是延伸自 q1,但 q1 不受後者影響。

QuerySets are lazy

建立查詢集的動作並不包含任何對資料庫的操作,不管如何堆疊過濾器也一樣。直到查詢集被計算 (evaluated),才會對資料庫做存取。

>>> 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(q)。一般來說,直到你真正要取值,查詢集是不會對資料庫做查詢的。

參考:When QuerySets are evaluated

get 取出單一物件

filter()exclude() 等等的過瀘器總是回傳 QuerySet。即便只有一個物件符合查詢結果,仍會回傳只有該物件的集合。

若確認只有唯一一個物件符合查詢,可以使用 Managerget() 方法,直接回傳查詢物件。

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

任何用在過濾器上的查詢表示式都可以套用在 get() 上,語法一樣參考 Field lookups

get()filter() 在使用上的一個不同是其回傳值。若沒有符合的查詢結果,get() 會回傳 DoesNotExist 的例外,相反的,若用 get() 查詢的結果超過一筆,也會回傳例外 MultipleObjectsReturned

DoesNotExistMultipleObjectsReturned 都是資料模型類別的屬性,以上個例子來講,就是Entry.DoesNotExistEntry.MultipleObjectsReturned

Other QuerySet methods

除了 all()filter()exclude() 外,還有許多查詢方法,完整的 QuerySet 方法參考 QuerySet API Reference

Limiting QuerySets

使用 Python 的部分 array-slicing 語法來限制 QuerySet 的輸出結果數量。這等價於 SQL 的 LIMITOFFSET 語法。

回傳前 5 筆查詢結果 (LIMIT 5):

>>> Entry.objects.all()[:5]

回傳從第 6 到第 10 個物件 (OFFSET 5 LIMIT 5)

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

但像是回傳最後一個物件用的 negative indexing 就不支援

>>> Entry.objects.all()[-1]

一般來說,切割 QuerySet 回傳的是新 QuerySet,一樣是真正要取值時才計算。一個例外是使用步進參數 (step parameter):

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

這種情況下,會確實執行查詢,回傳前 10 筆以 2 為步進的清單。

要取得單一物件,而不是整個清單的話 (如 SELECT foo FROM bar LIMIT 1),直接使用 array index 來取代 slice:

>>> type(Entry.objects.order_by('headline'))
django.db.models.query.QuerySet
>>> e1 = Entry.objects.order_by('headline')[0]

這大致上等同於:

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

e1 是在回傳的 QuerySet 上使用 array index,e2 是在 QuerySet 上使用 index slicing 縮小到只有一個元素的集合再用 get() (注意前面有提到,對 QuerySet 做 slicing 後仍是 QuerySet,可以使用 get())。

也因如此,二者在遇到沒有符合的查詢結果時回傳的例外不一樣,前者回傳 IndexError,後者回傳 DoesNotExist

In [53]: e = Entry.objects.filter(headline__startswith='abc')

In [55]: e
Out[55]: []

In [56]: e[0]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-56-3c9af7fabe24> in <module>()
...
...
...
IndexError: list index out of range

對前者來說,Entry.objects.filter(headline__startswith='abc') 本來就是空集合 QuerySet,再用 array index 只是從一個沒有內容物的集合中取物,所以得到 IndexError 這個例外。

而後者,一個空的 QuerySet 再經 slicing,仍舊是 QuerySet,雖然可以使用 get() 方法,但因沒有物件,所以回傳 DoesNotExist 這個例外。

Field lookups

欄位查詢對應到 SQL WHERE 語法。它們以關鍵字參數 (keyword argument) 指派到 QuerySet 方法中。

基本的查詢關鍵字參數使用 field__lookuptype=value 的格式,也就是欄位名稱透過雙底線 __ 接查詢種類。

>>> Entry.objects.filter(pub_date__lte='2006-01-01')

大概等價於

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01'

能辦到這一點是因為 Python 能定義接受任意 key-value 參數的函式,並在 runtime 期間對這些參數進行評估。參考官方文件 Keyword Arguments

查詢中用的欄位必須是 model field 的名稱。但使用外鍵時可以在欄位名稱後接 _id

In [74]: b = Blog.objects.all()[0]

In [75]: b
Out[75]: <Blog: Beatles Blog>

In [76]: Entry.objects.filter(blog=b)
Out[76]: [<Entry: Beatles is dead.>]

In [77]: Entry.objects.filter(blog_id=b.id)
Out[77]: [<Entry: Beatles is dead.>]

注意,在使用後綴 _id 的情況下,參數值得是關聯 model 的主鍵值:

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

若傳不合的 keyword argument,會回傳 TypeError

整個資料庫 API 提供的查詢類型大約超過二打,完整的清單參考 field lookup reference

以下列出較常使用的查詢…

回傳精確符合的結果

>>> Entry.objects.get(headline__exact="Man bites dog")

對應的 SQL 語法是

SELECT ... WHERE headline = 'Man bites dog';

當只提供欄位,而沒有再用雙底線後接查詢類型,預設使用 exact lookup type。

舉例來說,以下兩個查詢是等價的:

>>> Blog.objects.get(id__exact=14)  # Explicit form
>>> Blog.objects.get(id=14)         # __exact is implied

大小寫不分 (case-insensitive) 的精確符合查詢

>>> Blog.objects.get(name__iexact="beatles blog")

大小寫有分 (case-sensitive) 的「包含」查詢:

Entry.objects.get(headline__contains='Lennon')

大約等價於以下的 SQL 語法:

SELECT ... WHERE headline LIKE '%Lennon%';

同樣的,也有不分大小寫的版本 icontains

startswith, endswith, istartswith, iendswith

完整的查詢清單:field lookup reference

在此列出 1.6 版的 field lookups 完整清單,共 25 個:

  • exact
  • iexact
  • contains
  • icontains
  • in
  • gt
  • gte
  • lt
  • lte
  • startswith
  • istartswith
  • endswith
  • iendswith
  • range
  • year
  • month
  • day
  • week_day
  • hour
  • minute
  • second
  • isnull
  • search
  • regex
  • iregex

Lookups that span relationships

Django 也提供欄位後加雙底線的方式延展關聯,自動實現 SQL JOINs 的行為。

下例說明如何查詢所有外鍵關聯 Blogname 為 'Beatles Blog' 的 Entry 物件。

>>> Entry.objects.filter(blog__name='Beatles Blog')

同樣的,也能反查 (reverse relationship):

Blog.objects.filter(entries__headline__contains='Beatles')

當查詢跨越多層關聯,而其中有的 model 值沒有符合過濾條件,Django 會將其視為一個空 (卻合法,所有值都為 NULL) 的物件。沒有例外錯誤會出現。

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

上面的例子中,若一個 Entry 沒有任何 author 與其有關聯,會被視為也沒有 name 附加在其上。以此取代發出沒有 author 的例外。

但有一種查詢的使用會讓人混淆

Blog.objects.filter(entries__authors__name__isnull=True)

這樣的查詢不只回傳對 author 是空白的 name,也回傳對 entry 是空白的 authorBlog 物件。如果不想要後者,可以改寫成

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

Spanning multi-valued relationships

在對多對多欄位或反向外鍵做查詢時,有兩種不同的過濾器可以用。

考慮 Blog/Entry 關聯 (BlogEntry 是一對多)。我們可能想找出哪些 Blog,其 Entryheadline 為 "Lennon", (and)在 2008 年發表。或是想找出有哪些 Blog,它的 Entry headline 為 "Lennon" (or) 是在 2008 年發表的。

同時的情況跟需求也出現在多對多欄位。舉例,若一個 Entry 有個多對多欄位 tags,我們可能會想找出有哪些 entry 有 'music' 跟 'bands' 這兩個 tag。又或許我們想找出哪些 entry 有 tag 'music',且 status 欄位為 public

簡單來說,一個是且(and),另一個是或(or) 的情況。

的做法是把所有條件都塞在一個 filter 裡:

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

的做法是串接 filter。串接 filter 的用法在做關聯查詢時的意義跟之前提到的不同。原本的串接用法,會進一步縮小查詢集。但若 filter 內的 lookup 是針對關聯,作用的是任何跟主 model 有關的,而不是針對先前一個 filter 輸出的 QuerySet 做查詢。

Blog.objects.filter(entry__headline__contains='Lennon').filter(
        entry__pub_date__year=2008)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment