正確理解 Django ORM 中的 _、__、.
一次整理 user_id、user.id、user__id
在 Django 程式碼中,你可能已經見過這些寫法。
Post.objects.filter(user_id=1)
Post.objects.filter(user__id=1)
post.user.id
post.user_id
因為同時混用了底線 _、雙底線 __ 與點 .,很容易產生混淆。若不小心把它們混用,會帶來微妙的行為差異甚至性能問題。
本文將說明:
- Django ORM 中
_/__/.各自的確切含義 user_id/user.id/user__id的 意義與性能差異- 實務上 何時使用哪一種 的判斷標準
1. 大圖景:三種符號的角色
先簡單歸納三者:
_(單底線)- 只是名稱的一部分。
- 例:
created_at、user_id -
在 Django 中,
ForeignKey會自動產生<欄位名>_id的實際資料庫欄位。 -
__(雙底線) - Django ORM 專用的「查詢分隔符」。
- 只在
filter()、exclude()、order_by()、values()等方法的關鍵字參數名稱內有意義。 -
例:
age__gte=20(欄位 + 條件)user__email='a@b.com'(關聯後再存取其他欄位)created_at__date=...(轉換/變形後的條件)
-
.(點) - Python 的屬性存取運算子。
- 例:
obj.user、obj.user.id、qs.filter(...).order_by(...)等皆使用點運算子。 - 在 ORM 角度,這只是「存取記憶體中的 Python 物件屬性」;若需要,會觸發額外查詢(延遲載入)。
接下來從 Django ORM 的角度更詳細說明。
2. _ 與 _id:ForeignKey 周圍的意義
2.1 模型定義範例
from django.contrib.auth import get_user_model
from django.db import models
User = get_user_model()
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
這樣定義後,資料庫會產生類似以下欄位:
post表id(主鍵)user_id(實際 FK 欄位名稱)title
Django 會:
- 在 Python 物件上建立
post.user(User 物件) - 同時建立
post.user_id(整數或 PK 型別)
即:
post.user # -> User 物件(必要時會額外查詢 1 次)
post.user_id # -> User 的 PK 值(已存在,無額外查詢)
2.2 查詢中使用 _id
對於 ForeignKey user,可以用多種方式查詢:
# 1) 傳入 User 物件
Post.objects.filter(user=request.user)
# 2) 傳入 PK 值(同樣有效)
Post.objects.filter(user=request.user.id)
# 3) 直接使用 _id 欄位名稱
Post.objects.filter(user_id=request.user.id)
這三種方式最終編譯為相同的 WHERE 條件(簡單 PK 比較)。
小結:
_id是 * 「FK 欄位在資料庫中的實際名稱」 * ORM 中「直接使用該欄位」的名稱。
3. __ 雙底線鏈接:Django ORM 查詢分隔符
以下為 Django 專屬語法。
Django 官方文件稱 __ 為 「ORM lookup separator」。它用於連接欄位、關聯、條件或轉換。
3.1 基本模式
格式大致分為三種:
欄位名__lookup* 例:age__gte=20、name__icontains='kim'關聯欄位__其他欄位* 例:user__email='a@b.com'、profile__company__name='...'欄位__transform__lookup* 例:created_at__date=date.today()、created_at__year__gte=2024
範例:
# 1) 簡單比較
Post.objects.filter(title__icontains="django")
# 2) 關聯後條件
Post.objects.filter(user__email__endswith="@example.com")
# 3) 取日期欄位的年份
Post.objects.filter(created_at__year=2025)
若使用 user__email 這類關聯,資料庫層面會產生 JOIN。
核心:
__出現時,ORM 會在生成 SQL 時執行更複雜的操作(JOIN / 函式 / 條件)。
4. . 點運算子與 user.id、user_id
現在一起看 user.id 與 user_id。
4.1 Django 實例中的差異
post = Post.objects.first()
post.user # User 物件(必要時會額外查詢 1 次)
post.user.id # 上面取得的 User 物件的 PK
post.user_id # Post 表中已存在的 FK 值
重點:
- 第一次存取
post.user時,如果 User 尚未載入,會觸發額外查詢(延遲載入)。 post.user_id已在post物件中,無需額外查詢。- 因此「只需要 PK 值」時,使用
user_id更輕量。
4.2 性能範例:列表頁面
# views.py
posts = Post.objects.all() # 未使用 select_related
for post in posts:
print(post.user.id)
- 取得
posts的查詢:1 次 - 在迴圈中第一次存取
post.user時,每次都會額外查詢 User 表 → 若 Post 有 100 筆,User 查詢最多 100 次(典型 N+1 問題)
相反:
for post in posts:
print(post.user_id)
user_id已在 Post 表中,無額外查詢。
當使用 select_related("user") 預先 JOIN 時,即使使用 post.user 也不會額外查詢:
posts = Post.objects.select_related("user")
for post in posts:
print(post.user.id) # 無額外查詢
5. user_id、user.id、user__id 的精確比較
現在把三者放在一起比較。
5.1 user_id – 直接使用資料庫欄位
- 在哪裡?
- 模型實例:
post.user_id - ORM 查詢:
filter(user_id=1)、order_by("user_id")等 - 意義
- 直接使用
post表的實際 FK 欄位user_id。 - 性能
- 實例讀取時無額外查詢。
- 查詢時編譯為
WHERE "post"."user_id" = 1。
5.2 user.id – 透過關聯取得 PK
- 在哪裡?
- 只在 Python 物件中:
post.user.id - 意義
- 先取得
post.user(User 物件),再讀取其id。 - 性能
- 若
post.user尚未載入,會觸發額外查詢。 - 若使用
select_related("user"),則已在同一查詢中載入,無額外查詢。
小結: * 只需要值 →
user_id* 需要 User 的其他欄位或已使用select_related→user.<欄位>
5.3 user__id – ORM 查詢中的關聯條件
- 在哪裡?
- 僅在 ORM 查詢方法的關鍵字參數名稱中:
Post.objects.filter(user__id=1) - 意義
- 透過
user關聯,對 User 表的id欄位加條件。 - 執行方式
- 會產生 JOIN:
JOIN user ON post.user_id = user.id,然後WHERE user.id = 1。 - 實際 SQL
- 在簡單情況下,ORM 可能優化為
user_id = 1,但概念上仍是「關聯查詢」。
6. _ 與 __ 可能混淆的模式整理
6.1 常見篩選組合
# 1) 直接使用 FK 欄位名稱 (_id)
Post.objects.filter(user_id=1)
# 2) 傳入 FK 欄位的 PK 值
Post.objects.filter(user=1)
# 3) 關聯後加條件 (__)
Post.objects.filter(user__id=1)
Post.objects.filter(user__email__icontains="@example.com")
實務上常見的判斷:
- 只用 FK 的 PK →
user_id=...或user=request.user - 需要相關模型的其他欄位 →
user__email=...、user__is_active=True等,使用__。
6.2 _ 是「名稱的一部分」,__ 是「ORM 運算子」
_(底線)- 例:
created_at、user_id、is_active - 出現在 Python 變數、模型欄位、資料庫欄位等任何地方。
__(雙底線)- 只在 ORM 查詢方法的關鍵字參數名稱內有特殊意義。
- 用於「欄位 → 關聯 → 轉換 → 條件」的連接。
7. 實務中值得記住的規則總結
-
只用 FK 值時
python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id)兩者皆可,依團隊慣例選擇;個人偏好是傳入物件 (user=request.user) 以提升可讀性。 -
根據相關模型的其他欄位篩選時
python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com")這時__鏈接發揮作用。 -
模板/視圖中只需要 FK 值時
django {{ post.user.id }} {# 可能產生額外查詢 #} {{ post.user_id }} {# 更輕量 #}若未使用select_related("user"),列表頁面易出現 N+1 問題。 -
頻繁使用 User 的多個欄位時
python posts = Post.objects.select_related("user") for post in posts: print(post.user.username, post.user.email)select_related+post.user.<欄位>組合最乾淨且查詢數量最少。 -
__鏈接可能導致 JOIN 數量增加 * 特別是反向 FK 或多次filter().filter()鏈接,可能產生重複 JOIN,導致結果重複或效能下降。
8. 一句話總結
_:只是名稱,尤其<欄位名>_id是 FK 的實際資料庫欄位。__:ORM 專用運算子,連接欄位、關聯、條件、轉換。.:Python 屬性存取。user.id先載入 User 物件,再讀取 PK。user_id:Post 表的 FK 值。user.id:User 物件的 PK。user__id:ORM 查詢中「關聯後對 User.id 加條件」。
正確區分這三者,可讓查詢意圖更清晰,減少不必要的 JOIN 或 N+1 查詢,並在團隊內更順暢地進行程式碼審查。
目前沒有評論。