Django ORM 外鍵查詢指南
文章目錄
Let's Django!
這是 Django Tutorial 的第 8 篇、Django ORM 外鍵教學的第 3 篇——完結篇。
範例程式碼可參考我的 GitHub 專案。
系列:Django ORM 外鍵教學
- Django ORM 外鍵教學:設定一對一、一對多關聯
- Django ORM:反向關聯(Reverse relationship)介紹
- Django ORM 外鍵查詢指南
經過前 2 篇的鋪墊,我們可以真正開始感受,使用 ORM 來查詢 db 關聯物件的方便與直觀之美。
開始前,我們要先匯入範例資料,方式請參考〈用 Django Fixture 匯入與導出資料〉介紹的 Django fixture 與資料內容。
打開範例專案,cd 至專案根目錄,並使用指令:
1 | python manage.py loaddata post_data.json |
好,現在我們的 db 已經有資料了。
如果你已經不記得具體有哪些 table、它們代表什麼,可參考第一篇的模型介紹,以及第二篇對模型架構的調整,或直接觀看 models.py 原始碼。
本文主旨
本文只專注介紹 Django ORM 中的外鍵關聯查詢。
畢竟 Django ORM 的查詢語法實在太多了,很多時候都要回去看文件。
而其中關聯查詢特別常用,值得我們專門學習,熟練掌握。
範例資料簡介
目前 db 中有 3 個 table,範例資料是由這段程式碼所建立:
1 | from post.models import Post, Subtitle, Comment |
範例設計
如你所見,總共有 3 篇文章,其中 2 篇有副標題。這些文章共計有 5 篇留言,分屬於不同的文章。
為了確保教學內容聚焦在「外鍵關聯查詢」本身,我設計的範例資料相對單純,且筆數很少。以免你光要看懂範例資料就花掉好些時間。
不過它們依舊很有代表性!
這些 db 實例覆蓋了以下幾個重要的場景:
- 多個文章(Post)實例,每個文章有不同的標題和內容。
- 文章和副標題(Subtitle)的一對一關聯,其中有文章有副標題,有的沒有,這可以用來展示一對一關聯的查詢。以及「關聯不存在」的情況。
- 文章和留言(Comment)的一對多關聯,每個文章有不同數量的留言,這可以用來展示外鍵關聯的查詢。
使用__
進行 Django ORM 查詢
開始學習前,還有另一件重要的事要提醒。
Django ORM 會大量使用雙底線(__
)進行查詢。因此,你的 Django 模型欄位名稱不可以有雙底線。
而這些查詢主要分為兩大類。
一、以特定條件查詢欄位
利用雙底線來指定欄位的查詢條件,例如等於、不等於、大於、小於等。
常見的查詢條件有:__exact
、__iexact
、__contains
、__icontains
、__gt
(大於)、__gte
(大於等於)、__lt
(小於)、__lte
(小於等於)、__in
、__isnull
等。
實例如下:
1 | # 查詢標題包含 'Python' 的文章,不區分大小寫 |
二、外鍵關聯查詢
外鍵關聯查詢,就是「跨 table」的查詢。只用一次查詢,就能同時對兩個 table 進行條件過濾並獲得結果。
此時的雙底線,代表的是外鍵欄位的屬性。比如post
是Comment
的外鍵欄位,而post__title
指的是Post
的title
欄位。
接來就用這些資料,示範常見的外鍵關聯查詢。
附帶一提,第二篇使用反向關聯欄位的方式,也可以算是外鍵關聯查詢的一種。
了解雙底線的兩大用途後,才不會混淆查詢條件和關聯查詢。
以下例子不算複雜,但也未必很簡單。畢竟我們要展示的是「關聯查詢」,而不是單純的查詢。
第二篇介紹的「反向關聯」,是必須熟悉的內容。因為前兩個例子都與反向關聯有關。
讓我們開始吧!
一、查詢「有副標題」的文章
文章有副標題,代表文章和副標題之間「存在」一對一關聯。
這裡的重點:我們可以用isnull
作為查詢條件,判斷「關聯是否存在」。
1 | from post.models import Post |
subtitle
是Post
的一對一反向關聯欄位,查詢方式是subtitle__isnull
。
值得注意的是,如果subtitle
是普通欄位或正向關聯欄位,那相同寫法的查詢效果,將截然不同——會變成限制欄位值是否為None
。
當然,正向關聯欄位的值若為None
,也是代表(正向)關聯不存在。不過這與反向關聯不存在,本質上仍有所區別。
查詢結果:
1 | posts_with_subtitles |
簡言之,isnull
很常用來查詢「關聯是否存在」,這在外鍵關聯查詢中非常實用。
二、查詢留言中有「Thanks」一詞的文章
以「反向關聯欄位」進行查詢,我們來看第二種情況。
查詢留言中包括特定字詞,比如「Thanks」的文章。
1 | posts_with_thankful_comments = Post.objects.filter( |
comments
是Post
的反向關聯屬性,和前面的subtitle
類似。只不過這裡是一對多關聯。
這裡的重點是,related_name
不僅可以用來訪問 post.comments
,也可以直接用在查詢中,就像上面寫的那樣。
因為它也是一個外鍵關聯屬性——反向關聯屬性。
查詢結果:
1 | posts_with_thankful_comments |
三、查詢 id 為 1 的文章留言數
無論是一對一或一對多,當查詢對象是外鍵關聯欄位時,可以直接以__
查詢並過濾該外鍵欄位「值」(即另一個 db 實例)所擁有的欄位名稱。
1 | from post.models import Comment |
Comment
有一個外鍵欄位為post
,指向一個Post
實例。我們可以直接使用__
,以 Post
自己的屬性——本例中為id
,作為查詢條件。
查詢結果:
1 | comment_count |
這種以「正向關聯欄位」的屬性(比如post
的屬性id
)作為條件的查詢,佔了關聯查詢需求的很大一部分。
查詢外鍵 Primary Key 的不同變體
上述的id
是外鍵post
的主鍵(Primary Key),用id
查詢的情況很常見。
因此,用外鍵的主鍵(Primary Key)欄位查詢時,Django 幫你準備了多種寫法。以下寫法效果皆相同:
1 | # 使用 post__id 原來的寫法 |
Django 會自動為外鍵加上_id
,代表該外鍵的主鍵,命名模式為<外鍵名稱>_id
。且無論該外鍵欄位主鍵名稱是什麼,都可以使用這種寫法。
同理,post__pk
也是類似效果,它們都算是一種欄位別名。
雖然用別名的寫法讓人感覺更加「道地」、更像內行人。但如果想保守起見,還是使用post__id
比較好。因為這種寫法最直觀,而且適用於所有欄位。
我個人認為,不用別名也沒關係,explicit 至上。
何時使用哪種寫法?
Django 入門者最常使用的是第 4 種,它很好懂,因為欄位值正是一個 db 物件!
然而,它需要程式碼上下文中,先有一個post
物件。如果沒有,就要像上面寫的,先透過查詢取得post
物件——這會增加一次 db 查詢。
如果當前上下文只有post
的 id,強烈建議使用前 3 種寫法(個人推薦第 1 種),以減少不必要的查詢負擔。
第 5 種寫法雖然也「很 Django」,但很多人不熟悉,所以我不敢輕易採用。
四、查詢標題有「Django」的所有留言
上面「三」是用 id 查詢,這裡改用特定字詞查詢。
而且,不是直接查詢標題中包含「Django」的文章(這和外鍵查詢無關),而是文章標題中包括 Django 的所有留言。
1 | comments_for_django_posts = Comment.objects.filter( |
「二」的comments__content__icontains
,和這裡的post__title__icontains
,有一個區別——前者是反向關聯欄位,後者是正向關聯欄位。
兩者都用了經典的「兩次雙底線」查詢,即post__title__icontains
。
這樣的語句看起來有點「抽象」,這也是為什麼我們要把雙底線查詢分成兩大類,因為它們可能會同時出現!
弄清楚兩者之間的區別,才更容易明白,兩次雙底線各代表什麼查詢條件:
post__title
:查詢Post
的title
欄位。title__icontains
:查詢title
欄位中包含特定字詞(不分大小寫)的文章。
查詢結果:
1 | comments_for_django_posts |
查詢結果的變數命名
所有的 ORM 查詢結果的變數,只要結果是 0 到多個——也就是 QuerySet,請務必使用「複數」命名,比如comments
、posts
。
只有在確定「最多只會拿到一個」db 實例時,才用單數。這通常只發生在,你使用了get
或first
、last
等直接回傳實例的 QuerySet 方法。
本文中的查詢結果變數命名都較為冗長,主要是為了教學上的表達。
實務中我通常不會把查詢條件一一映射到變數命名,除非該條件、特性非常重要。
結語:Djangonic
本文僅涵蓋了 Django ORM 外鍵查詢的一部分,但都是經典且常用的場景,有一定的代表性。
善用這些關聯查詢的重點在於使程式碼更加直觀和優雅。
如第二篇文章所述,其實這些查詢也可以「不透過」外鍵關聯查詢來實現,但可能會讓程式碼變得冗長,甚至難以理解。
熟練的 Python 開發者可以達到 Pythonic 的境界,在 Django 中也有類似 Pythonic 的術語,稱為「Djangonic」或「Djangonic way」。
這種風格強調利用 Django 的內建功能和最佳實踐,以簡化程式碼和提高可維護性。
這也是我們作為 Django 開發者的日常追求。
相關文章