Let's Django!Let's Django!

這是 Django Tutorial 系列連載的第 1 篇。

搭配學習的範例程式碼,可參考 GitHub 專案:Django-Tutorial

系列:Django ORM 外鍵教學

  1. Django ORM 外鍵教學:設定一對一、一對多關聯
  2. Django ORM:反向關聯(Reverse relationship)介紹
  3. Django ORM 外鍵查詢指南

工作上使用 Django 近 2 年,卻很少發表關於 Django 主題的文章,是時候該來補一補了。

那就從 Django ORM 開始吧!因為 ORM(Django Models)可以說是無論你怎麼使用 Django(全端或前後端分離),都不得不學的核心部分。

怎麼說?我們先來看看,在「前後端分離」的開發趨勢下,Django 的三大核心——MTV——的重要性有哪些變動。


前後端分離下的 Django MTV

在 Django 中,MTV 指的是「Model-Template-View」架構模式。類似於 MVC,這是 Django 框架的核心設計模式,用於組織和分離應用程式的不同部分。

  • Model(模型): 模型負責處理與資料庫相關的操作,它是定義數據結構、資料表和資料庫查詢的地方。
  • Template(模板): 模板用於前端。它是一個包含 HTML、CSS 和一些額外標記語言(例如 Django 的模板語言)的文件,用於定義網頁的外觀和內容。
  • View(視圖): 視圖是應用程式處理邏輯的地方。它接收來自前端的 HTTP 請求,並根據需要調用相應的模型和模板來生成回應。

前後端分離對三大元件的重要性影響

假設你是一個前後端分離下的 Django 工程師,那通常是負責後端的 API 開發

首先,顯然 Template 不需要了,因為這是前端的範疇,而 View 呢?相當程度會被 Django REST framework(以下皆簡稱為 DRF)所取代。說「取代」不太正確,應該說「升級」。

帶來的影響是 View 中相當部分操作細節與學習重點會遷移到 DRF 本身,而非 Django 提供的原生功能。

比如request.data,重新封裝了 Django 中的request.POSTrequest.FILES兩個屬性,類似的情況還有APIView@api_view等等。

既然 Template 被前端取代,而 View 的功能則被 DRF 重新封裝。可想而知,三大元件中重要性唯一不變的,就是 Model——Django ORM。

可見,作為一個 Django 開發者,學好 ORM 是絕對不虧的!

有助於學習 FastAPI

如果這樣還不夠,我再給你一個學好 Django ORM 的理由。(加分項)

現在火紅的 FastAPI,本身並沒有包括 ORM 功能,所以需要你另外選擇 ORM 套件。常見的選項有 SQLAlchemyTortoise ORM

而 Tortoise ORM 正是受到 Django ORM 的啟發,所以學好 Django ORM 之後,學習 Tortoise ORM 會更容易上手:

Tortoise ORM is an easy-to-use asyncio ORM (Object Relational Mapper) inspired by Django.

Tortoise ORM was built with relations in mind and admiration for the excellent and popular Django ORM. It’s engraved in its design that you are working not with just tables, you work with relational data.

附帶一提,我個人不太喜歡 SQLAlchemy,因為它的語法相對複雜,還有那過時的官方文件網站排版(這點和 DRF 很像XD)。所以推薦 Tortoise ORM。

PS:但是!如果你是要用在正式產品環境中,那麼 SQLAlchemy 可能是更好——至少是更穩健——的選擇。畢竟它發展多年,而且有著龐大的社群支持。這點不是 Tortoise ORM 可以比擬的。


範例程式碼專案介紹

如前所述,我打算寫一系列的 Django 教學文章,有範例程式碼,會更方便讀者學習、參考。於是我建了一個 GitHub Repo 名為「Django-Tutorial」,把文章中使用的程式碼同步更新於此。

這是一個典型的 Django 專案,而且有著完整的 Python 開發環境設定,各種細節都和真實世界一致:Poetry、pre-commit 與 linter、formatter(目前已改用 Ruff)等,一應俱全,方便你重現環境並跟著操作。

相關文章:

以下是簡單的介紹。

專案開發環境介紹

支援 Poetry,方便重建專案所需的 Python 虛擬環境。但你也可以不使用它,我有另外準備requirements.txtpip安裝。

pre-commit 完全可選,基本上用不到,除非你有打算變更程式碼的內容。只要不使用指令pre-commit install,它相當於不存在。但如果你想用的話,整個.pre-commit-config.yaml設定檔都寫好了。

隨著文章更新,未來還會支援 Docker,敬請期待。

工具版本說明

Django 版本為 4.2 LTS,對 Python 的版本有一定要求,不能太舊,要 3.8 以上。

Python 版本使用 3.10.11,建議至少使用 3.8.1,雖然剛剛說 Django 只要求 3.8,但因為 Flake8 版本是 6.0.0(已改為 Ruff),要求 Python 至少要 3.8.1 XD。

總之,強烈建議直接安裝 3.10 或更新的版本。


基本的介紹就到這裡,我們進入本篇重點。

本文重點:Django ORM 外鍵關聯設定

Django Models,也就是 database table 的 OOP 型態,透過 ORM 來實現兩者的對應關係——從 table 到 Python class。

如果說 ORM 是 Django 的核心之一,而 ORM 的核心會是什麼呢?我相信其中一個答案,就是「外鍵關聯」(relationships)。若是少了關聯,資料表就像孤立的島嶼,根本無法真實模擬世界。

剛開始寫 Django ORM 時,疑問最多的就是外鍵關聯了!因為它不止重要,而且十分常用。外鍵關聯又分一對一、一對多(多對一)與多對多,我們會先著重在前兩者的介紹——因為它們最常遇到,方便你快速上手。

本文為上篇,主要是系列前言與外鍵設定教學,查詢部分則留到下篇。

專案情境介紹

學習 Django 模型和外鍵關係的最佳方法,是透過具體的例子。在上述專案中,我準備了一個最常見的範例:文章模型

選這個例子主要出於兩個原因:

  1. 它很容易想像。
  2. 它同時包括了一對一、一對多等不同情境。

模型對應的情境大致是這樣:你有一個部落格網站,你可以發表文章,而讀者可以留言。

因此,具體情境與對應的模型關係如下:

  • 一篇文章只有一個標題,但標題可以有主標題和副標題。文章和標題(含副標題)是一對一關係
  • 一篇文章可以有多個留言,但一個留言只能屬於某一篇文章。文章和留言是一對多關係

上述設計主要是為了教學與說明方便,不必對它們的真實性太過認真。

並且,簡化過的情境與模型,有助於我們專注於外鍵關聯的設定,而不會被其他細節所干擾

專案模型介紹

以下我們一一介紹這 3 個模型。

Title:文章標題

通常標題只會是文章模型的一個欄位,很少獨立出來。我這樣設計是為了呈現一對一關係,而且這裡有分主、副標題,多少為獨立出來增加了一些合理性。

Post:文章

最主要的模型,其餘兩個模型都和它有關。

與標題是一對一關係,直接有一個title外鍵欄位關聯到 Title 模型,另一個欄位則是content

Comment:留言

用來說明對一多關係的模型,有一個外鍵欄位post關聯到 Post。對 Post 來說,則會產生一個「反向關聯reverse relationship)」屬性,下面會詳細介紹。

三者的關係可以畫成簡單的實體關聯圖(Entity Relationship Diagram,簡稱 ERD),如下。可留意關係線圖的表示方式,這屬於 ERD 的規範:

Django 會自動幫你在外鍵屬性名稱加上_id,轉換成 db 中 table 欄位的名稱,所以上面圖中的欄位名稱是title_idpost_id


在 Model 中建立外鍵欄位

我們知道,ORM 所對應的 table 欄位,都是用 Python 類別中的類別屬性來定義與規範的。而 db 欄位的 schema 則對應 model 屬性的「參數」。

尤其是外鍵屬性,因為要建立關聯,使用的參數通常比較多,格式上也和一般欄位屬性有所差別。

無論如何,了解外鍵欄位常用的參數與其代表的意義,相當必要,這也是本篇的重心。

以下介紹一對一和一對多的外鍵關聯設定,讀者可適時參考深獲開發者好評的 Django 文件,我們只就重點進行說明。

一對一關係

參考OneToOneField文件,有兩個必填的位置參數,即toon_delete

先看一下專案中存在一對一關係的兩個 model(為了網頁呈現,我縮減了單行字元上限與空行數、省略了無關部分,所以和原始碼有所不同):

1
2
3
4
5
6
7
8
9
10
from django.db import models

class Post(models.Model):
title = models.OneToOneField(
'Title', on_delete=models.PROTECT, related_name='post')
content = models.TextField()

class Title(models.Model):
main = models.CharField(max_length=100)
subtitle = models.CharField(max_length=100)

OneToOneField即外鍵中的一對一關係欄位,第一個參數是to,為目標關聯的 model,有兩種格式:

  1. model class 本身。
  2. 字串。用於無法直接引用的情境,比如本例中的 title 欄位,Title類別定義在Post之後。

第二個參數是on_delete,用來定義「關聯物件被刪除時」當前物件該如何處理的行為。有多揰模式,最常用的不外乎CASCADEPROTECTSET_NULL這 3 種,其餘所有選項與行為定義,可以參考文件

除了toon_delete兩個必要參數,剩下的都是 optional,不過還有一個參數也非常重要,那就是related_name


related_name用於指定關聯目標 model(本例為Title)的「反向關聯」屬性名稱。這個屬性對查詢很實用,所以related_name也是外鍵屬性中重要的參數之一。

我們從 model 角度來看外鍵建立後的效果,以及related_name所扮演的角色。

Post 角度

Post有一個屬性為title,也就是我們所建立的外鍵,這個屬性在 model 中是明示的,意味著它在 db 中也會有對應的 table 欄位——title_id。其中_id是 Django 幫你加的,你可以透過class Meta自行定義這個外鍵欄位的名稱。

事實上,建立關聯不一定只能指向目標 model 的「主鍵」,只要是 model 中的 unique 欄位都可以,這部分可參考 to_field 文件

Title 角度

一對一關係建立後,對Title的 db 實例而言,它得到了什麼?——反向關聯屬性

這個屬性在 model 中沒有明示,你從 model 中看不出Title實例有什麼屬性可以指向Post。但實際上Title實例確實有一個反向關聯屬性指向Post

這就是反向關聯的特性——它是「隱式」的。即這個屬性確實存在,只是不以「欄位」的形式存在。


反向關聯屬性的名稱就是前述related_name所定義的名稱,即post

1
2
3
...
title = models.OneToOneField(
'Title', on_delete=models.PROTECT, related_name='post')

所以,Title的所有實例,比如有一個實例叫title_1,一定會有一個反向關聯屬性post,而它的返回值依不同情況有兩種可能

  1. title_1已經被關聯到某個 Post 實例比如post_1,那title_1.post的值就是該post_1實例。
  2. RelatedObjectDoesNotExist物件。當實例之間的關聯還不存在,試圖取得關聯實例將會出現這樣的錯誤。

附帶一提,RelatedObjectDoesNotExist其實就是ObjectDoesNotExist的子類別。所以如果你想要捕捉它,直接使用常見的ObjectDoesNotExist即可。比如:

1
2
3
4
try:
title_1.post
except ObjectDoesNotExist:
# do something

反向關聯屬性

上述post屬性並未在 model class 中明示(但物件有此屬性),資料表中也不存在相對應的欄位。它本質上只是一個「ORM 查詢捷徑」——但非常實用。

這種隱式的設計讓我們更容易識別在外鍵關係中,哪些屬性是正向關聯,哪些則是反向關聯

反向關聯允許我們透過簡單的屬性呼叫,就能夠輕鬆地獲取相關資料,而不需要額外的操作和 ORM 查詢語句,真的很方便。


一對多關係

class ForeignKey(to, on_delete, **options)

A many-to-one relationship. Requires two positional arguments: the class to which the model is related and the on_delete option.

我們在前述一對一關係費了很大的功夫將關聯的細節詳加說明,有了上述基礎,理解一對多關係也會容易得多。

Django 稱ForeignKey為「many-to-one」,即多對一關係,從 model 角度看,確實更合理。因為建立這個ForeignKey屬性的 model,必然屬於關係中的「多方」。所以是「多對一」。

不過無論一對多或多對一,主要區別是視角不同,都是同一種關係。下面我還是用「一對多」這個詞進行說明。

我選擇先講一對一是因為它相對單純,不需要一次理解太多事情。而一對多(或多對一),即 Django 中的 ForeignKey,則有更多參數和變化,但我們依舊只關注其中最重要的部分。

專案程式碼說明

毫無疑問,文章和它的留言是一對多關係,一篇文章可以有「0 到多個」留言,注意這個 0 還滿重要的!這也是它和一對一關係很不同的地方。

回到程式碼:

1
2
3
4
5
6
7
8
9
10
11
# 文章
class Post(models.Model):
title = models.OneToOneField(
'Title', on_delete=models.PROTECT, related_name='post')
content = models.TextField()

# 留言
class Comment(models.Model):
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name='comments')
content = models.TextField()

我們可以看到,Comment 有一個欄位叫post,是一個ForeignKey。這個ForeignKey欄位,最常用的參數還是那 3 個,前 2 個前面已經有介紹,在此不贅。

第 3 個參數仍是related_name,但它的引數值'comments'看起來,和一對一的related_name在命名上有所不同——它是複數!

前提已經提過,related_name實際上是為「反向關聯屬性」進行命名。

post屬性的related_name='comments'意味著,Post model 將得到一個名為「comments」反向關聯屬性。

一個 Post 實例,假設為post_1,可以透過post_1.comments取得所有和它關聯的 Comment 實例。可能有 1 個、多個,甚至沒有。

這裡有一個細節是,post_1.comments只會先取得「關係管理器」物件,再透過該物件取得「由 Comment 關聯實例組成的QuerySet」,比如:post_1.comments.all()

這和一對一關係中,你呼叫反向關聯屬性時,可能得到一個關聯實例或拋出RelatedObjectDoesNotExist有明顯不同。


如果你在建立欄位時沒有給定related_name引數,那 Django 會自動給你預設名稱。

一對一

在一對一中,預設名稱為外鍵欄位所屬 Model 名稱的小寫。

參考 Post 的一對一外鍵程式碼:

1
2
3
class Post(models.Model):
title = models.OneToOneField(
'Title', on_delete=models.PROTECT, related_name='post')

related_name的預設值就是 Post 的小寫——post。顯然我定義的其實就是預設值而已。一對一時,是否定義related_name影響不大,因為它的預設值往往就已足夠

一對多

在一對多,預設名稱為外鍵欄位所屬 Model 名稱的小寫再加上_set後綴。你可能就未必喜歡這樣的命名了。

一樣看一下相關程式片段:

1
2
3
class Comment(models.Model):
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name='comments')

如果沒有定義related_name,則被關聯的 Post 將獲得 Django 預設的反向關聯屬性名稱「comment_set」。

無論是自定義的comments或預設的comment_set,都表達它是一個「複數集合」,這是一對多的特色(多對多也是如此)。


實務上,賦與related_name良好、高可讀的命名是值得的。雖然大部分時候我們可以簡單使用複數即搞定,例如上面的comments名稱。

但當關聯較多、情況複雜的時候,為了維持程式的可讀性,就需要你花費一點巧思。

舉例而言,想像下列情境:任何專案都需要有憑證才能存取,而一張憑證可讓多個專案共用,此時我們可以這樣設計:

1
2
3
4
5
class Project(models.Model):
...
certificate = models.ForeignKey('Certificate',
on_delete=models.PROTECT,
related_name='used_by_projects')

這裡的 related_name 不再用死板板的projects來命名,因為當你透過Certificate物件要訪問「所有使用該憑證的專案」時,certificate.used_by_projects顯然會比certificate.projects更具描述性——尤其在Certificate還有更多其它關聯的時候,太多certificate.XXXs容易讓人混淆。

簡言之,used_by_projects命名讓程式更可讀,讓你一看就知道這個related_name所代表的意義與用途


小結:關聯是 Django Model 基石

耗費了如此多的幅篇,詳細講述關聯設定,都是為了強調一個重點:關聯是 Django ORM 的核心之一。

通過關聯,我們能夠模擬真實世界中的關係,使資料表之間建立起有意義的連結。

了解外鍵的關聯設定,以及反向關聯屬性的使用,我們能夠更好地應用 Django ORM,建構出高效且具有關聯性的資料模型。

而辛苦建立這些模型與關聯,就是為了能夠充分利用它們!

後續我們將探討,如何有效對這些關聯模型進行查詢,輕鬆地從模型中檢索和篩選所需資料。

下一篇:Django ORM:反向關聯(Reverse relationship)介紹