正確理解 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_atuser_id
  • 在 Django 中,ForeignKey 會自動產生 <欄位名>_id 的實際資料庫欄位。

  • __(雙底線)

  • Django ORM 專用的「查詢分隔符」
  • 只在 filter()exclude()order_by()values() 等方法的關鍵字參數名稱內有意義。
  • 例:

    • age__gte=20(欄位 + 條件)
    • user__email='a@b.com'(關聯後再存取其他欄位)
    • created_at__date=...(轉換/變形後的條件)
  • .(點)

  • Python 的屬性存取運算子
  • 例:obj.userobj.user.idqs.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 基本模式

格式大致分為三種:

  1. 欄位名__lookup * 例:age__gte=20name__icontains='kim'
  2. 關聯欄位__其他欄位 * 例:user__email='a@b.com'profile__company__name='...'
  3. 欄位__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.iduser_id

現在一起看 user.iduser_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_iduser.iduser__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_relateduser.<欄位>

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_atuser_idis_active
  • 出現在 Python 變數、模型欄位、資料庫欄位等任何地方。
  • __(雙底線)
  • 只在 ORM 查詢方法的關鍵字參數名稱內有特殊意義。
  • 用於「欄位 → 關聯 → 轉換 → 條件」的連接。

7. 實務中值得記住的規則總結

  1. 只用 FK 值時 python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id) 兩者皆可,依團隊慣例選擇;個人偏好是傳入物件 (user=request.user) 以提升可讀性。

  2. 根據相關模型的其他欄位篩選時 python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com") 這時 __ 鏈接發揮作用。

  3. 模板/視圖中只需要 FK 值時 django {{ post.user.id }} {# 可能產生額外查詢 #} {{ post.user_id }} {# 更輕量 #} 若未使用 select_related("user"),列表頁面易出現 N+1 問題。

  4. 頻繁使用 User 的多個欄位時 python posts = Post.objects.select_related("user") for post in posts: print(post.user.username, post.user.email) select_related + post.user.<欄位> 組合最乾淨且查詢數量最少。

  5. __ 鏈接可能導致 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 查詢,並在團隊內更順暢地進行程式碼審查。