Django Models 關聯關係:一對一、一對多、多對多完全解析 | Django 教學
在 Django 的 ORM(Object-Relational Mapping,物件關聯映射)中,Model 之間的關聯關係是資料庫設計的核心。Django 提供了三種關聯欄位:OneToOneField(一對一)、ForeignKey(一對多)和 ManyToManyField(多對多),讓你用 Python 程式碼就能表達資料表之間的關係,無需手動撰寫 SQL。這篇文章將完整解析每種關聯的定義方式、查詢技巧與實戰應用。
關聯關係概覽
在開始之前,先快速了解三種關聯的對應場景:
| 關聯類型 | Django 欄位 | 典型場景 |
|---|---|---|
| 一對一 | OneToOneField | 使用者 ←→ 個人資料 |
| 一對多 | ForeignKey | 作者 → 多篇文章 |
| 多對多 | ManyToManyField | 文章 ←→ 標籤 |
一對一關係(OneToOneField)
一對一關係(One-to-One Relationship)表示兩張資料表之間「一筆對一筆」的對應。最常見的應用場景是擴充 Django 內建的 User 模型,將額外的個人資料拆分到獨立的 Profile 資料表。
Model 定義
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
"""使用者個人資料(擴充 User 模型)"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE, # 刪除 User 時連帶刪除 Profile
related_name='profile' # 反向查詢名稱
)
bio = models.TextField(blank=True, verbose_name='自我介紹')
birth_date = models.DateField(null=True, blank=True, verbose_name='生日')
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
class Meta:
verbose_name = '個人資料'
def __str__(self):
return f'{self.user.username} 的個人資料'
正向查詢與反向查詢
# 正向查詢:從 Profile 取得 User
profile = Profile.objects.get(id=1)
user = profile.user # 直接透過欄位名稱存取
print(user.username)
# 反向查詢:從 User 取得 Profile
user = User.objects.get(username='john')
profile = user.profile # 透過 related_name 存取
print(profile.bio)
# 建立 Profile
profile = Profile.objects.create(
user=user,
bio='Hello, I am John!',
birth_date='1990-05-15'
)
注意:OneToOneField 的反向查詢回傳的是單一物件,而不是 QuerySet。如果對應的物件不存在,會拋出
RelatedObjectDoesNotExist例外。
一對多關係(ForeignKey)
一對多關係(One-to-Many Relationship)是最常用的關聯類型。一個作者可以有多篇文章,但每篇文章只屬於一個作者——這就是典型的一對多關係。在 Django 中,我們使用 ForeignKey(外鍵)來定義這種關係,外鍵欄位放在「多」的那一方。
Model 定義
from django.db import models
class Author(models.Model):
"""作者"""
name = models.CharField(max_length=100, verbose_name='姓名')
email = models.EmailField(unique=True)
class Meta:
verbose_name = '作者'
def __str__(self):
return self.name
class Post(models.Model):
"""文章"""
title = models.CharField(max_length=200, verbose_name='標題')
content = models.TextField(verbose_name='內文')
author = models.ForeignKey(
Author,
on_delete=models.CASCADE, # 刪除作者時連帶刪除文章
related_name='posts' # 自訂反向查詢名稱
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name = '文章'
def __str__(self):
return self.title
related_name 自訂反向查詢名稱
related_name 參數讓你自訂從「一」方反向查詢「多」方的存取器名稱。如果不設定 related_name,Django 預設使用 小寫模型名_set(例如 post_set):
# 未設定 related_name 時的預設反向查詢
author.post_set.all()
# 設定 related_name='posts' 後的反向查詢
author.posts.all()
建議總是明確設定 related_name,讓程式碼更易讀、更直觀。
正向查詢與反向查詢
# 正向查詢:從 Post 取得 Author
post = Post.objects.get(id=1)
author = post.author # 取得該文章的作者
print(author.name)
# 反向查詢:從 Author 取得所有 Post
author = Author.objects.get(name='王小明')
posts = author.posts.all() # 取得該作者的所有文章
published_posts = author.posts.filter(status='published')
# 建立文章並指定作者
post = Post.objects.create(
title='Django 入門教學',
content='這是一篇 Django 教學文章...',
author=author
)
# 跨表查詢(使用雙底線語法)
# 找出所有由 email 為 gmail 的作者撰寫的文章
posts = Post.objects.filter(author__email__endswith='@gmail.com')
多對多關係(ManyToManyField)
多對多關係(Many-to-Many Relationship)表示兩邊都可以有多個對應。一篇文章可以有多個標籤,一個標籤也可以被多篇文章使用。Django 使用 ManyToManyField 來定義這種關係,並會自動在背後建立一張中介表(Intermediate Table)。
Model 定義
from django.db import models
class Tag(models.Model):
"""標籤"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True)
class Meta:
ordering = ['name']
verbose_name = '標籤'
def __str__(self):
return self.name
class Post(models.Model):
"""文章"""
title = models.CharField(max_length=200, verbose_name='標題')
content = models.TextField(verbose_name='內文')
tags = models.ManyToManyField(
Tag,
related_name='posts', # 從 Tag 反向查詢 Post
blank=True # 允許文章沒有標籤
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = '文章'
def __str__(self):
return self.title
add()、remove()、clear() 操作
多對多關係不能在 create() 時直接指定,必須先建立物件後再操作關聯:
# 取得或建立標籤
python_tag = Tag.objects.create(name='Python', slug='python')
django_tag = Tag.objects.create(name='Django', slug='django')
web_tag = Tag.objects.create(name='Web', slug='web')
# 建立文章
post = Post.objects.create(title='Django 教學', content='...')
# add() — 新增關聯
post.tags.add(python_tag, django_tag) # 一次加入多個標籤
post.tags.add(web_tag) # 也可以逐一加入
# remove() — 移除關聯(不會刪除 Tag 物件本身)
post.tags.remove(web_tag)
# clear() — 清除所有關聯
post.tags.clear() # 移除該文章的所有標籤
# set() — 直接設定關聯(取代現有的)
post.tags.set([python_tag, django_tag]) # 只保留這兩個標籤
# 查詢文章的所有標籤
post.tags.all() # <QuerySet [<Tag: Django>, <Tag: Python>]>
# 反向查詢:查詢標籤下的所有文章
python_tag.posts.all() # 所有帶有 Python 標籤的文章
# 跨表查詢
Post.objects.filter(tags__name='Python') # 所有有 Python 標籤的文章
Tag.objects.filter(posts__title__icontains='django') # 被 Django 相關文章使用的標籤
on_delete 選項詳解
在定義 ForeignKey 和 OneToOneField 時,on_delete 是必填參數,它決定了當被參照的物件被刪除時,該如何處理關聯的物件。
| 選項 | 行為 | 使用場景 |
|---|---|---|
CASCADE | 連帶刪除(級聯刪除) | 刪除文章時一併刪除留言 |
PROTECT | 禁止刪除,拋出 ProtectedError | 作者有文章時不允許刪除作者 |
SET_NULL | 設為 NULL(需搭配 null=True) | 刪除分類後,文章的分類欄位設為空 |
SET_DEFAULT | 設為欄位的 default 值 | 刪除作者後,文章歸為「匿名作者」 |
SET(value) | 設為指定值或呼叫 callable | 自訂更靈活的處理邏輯 |
DO_NOTHING | 不做任何處理 | 自行處理資料一致性(不建議使用) |
實際範例
class Comment(models.Model):
# CASCADE:刪除文章時,留言也一起刪除
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments'
)
content = models.TextField()
class Article(models.Model):
# PROTECT:分類下有文章時,禁止刪除該分類
category = models.ForeignKey(
'Category',
on_delete=models.PROTECT,
related_name='articles'
)
# SET_NULL:刪除作者後,文章保留但作者欄位設為 NULL
author = models.ForeignKey(
'Author',
on_delete=models.SET_NULL,
null=True, # 必須允許 NULL
blank=True,
related_name='articles'
)
# SET_DEFAULT:刪除標籤後,設為預設標籤
primary_tag = models.ForeignKey(
'Tag',
on_delete=models.SET_DEFAULT,
default=1, # 預設標籤的 ID
related_name='primary_articles'
)
最佳實踐:優先考慮
CASCADE和PROTECT。如果不確定該用哪個,問自己一個問題:「刪除父物件時,子物件是否還有存在的意義?」有的話用PROTECT或SET_NULL,沒有的話用CASCADE。
through 中介模型(自訂多對多關係)
當多對多關係需要儲存額外資訊時,可以使用 through 參數指定一個中介模型(Intermediate Model)。例如:學生選課需要記錄選課日期和成績。
Model 定義
from django.db import models
class Student(models.Model):
"""學生"""
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Course(models.Model):
"""課程"""
name = models.CharField(max_length=200)
students = models.ManyToManyField(
Student,
through='Enrollment', # 指定中介模型
related_name='courses'
)
def __str__(self):
return self.name
class Enrollment(models.Model):
"""選課記錄(中介模型)"""
student = models.ForeignKey(
Student,
on_delete=models.CASCADE,
related_name='enrollments'
)
course = models.ForeignKey(
Course,
on_delete=models.CASCADE,
related_name='enrollments'
)
enrolled_at = models.DateTimeField(auto_now_add=True, verbose_name='選課時間')
grade = models.CharField(
max_length=2,
blank=True,
verbose_name='成績'
)
class Meta:
unique_together = ['student', 'course'] # 防止重複選課
verbose_name = '選課記錄'
def __str__(self):
return f'{self.student.name} - {self.course.name}'
使用中介模型操作
# 建立資料
student = Student.objects.create(name='王小明')
course = Course.objects.create(name='Python 程式設計')
# 使用 through 模型時,必須透過中介模型來建立關聯
enrollment = Enrollment.objects.create(
student=student,
course=course,
grade='A+'
)
# 查詢學生選了哪些課
student.courses.all()
# 查詢某堂課有哪些學生
course.students.all()
# 查詢特定選課記錄的額外資訊
enrollment = Enrollment.objects.get(student=student, course=course)
print(enrollment.grade) # A+
print(enrollment.enrolled_at) # 2026-05-29 10:30:00
# 篩選:找出成績為 A+ 的選課記錄
Enrollment.objects.filter(grade='A+').select_related('student', 'course')
注意:使用
through中介模型後,就不能再使用add()、create()、set()方法來操作關聯,必須直接透過中介模型來建立和管理。但remove()和clear()仍然可以使用。
自我參照關係(Self-Referential Relationship)
有時候一個 Model 需要參照自己,這種情況稱為自我參照關係(Self-Referential Relationship)。最常見的例子是員工與主管的階層關係、留言的回覆功能,或是社群媒體的好友/追蹤關係。
員工與主管(一對多自我參照)
class Employee(models.Model):
"""員工"""
name = models.CharField(max_length=100, verbose_name='姓名')
position = models.CharField(max_length=100, verbose_name='職位')
manager = models.ForeignKey(
'self', # 參照自己
on_delete=models.SET_NULL,
null=True,
blank=True, # 最高層主管沒有上級
related_name='subordinates' # 反向查詢:取得下屬
)
class Meta:
verbose_name = '員工'
def __str__(self):
return f'{self.name} ({self.position})'
# 建立組織架構
ceo = Employee.objects.create(name='陳總', position='CEO')
vp = Employee.objects.create(name='李副總', position='VP', manager=ceo)
dev_lead = Employee.objects.create(name='王經理', position='開發主管', manager=vp)
developer = Employee.objects.create(name='張工程師', position='工程師', manager=dev_lead)
# 查詢主管
developer.manager # <Employee: 王經理 (開發主管)>
developer.manager.manager # <Employee: 李副總 (VP)>
# 查詢直屬下屬
ceo.subordinates.all() # <QuerySet [<Employee: 李副總 (VP)>]>
vp.subordinates.all() # <QuerySet [<Employee: 王經理 (開發主管)>]>
社群追蹤(多對多自我參照)
class UserProfile(models.Model):
"""使用者資料(含追蹤功能)"""
username = models.CharField(max_length=100, unique=True)
following = models.ManyToManyField(
'self',
symmetrical=False, # 追蹤關係不對稱(A 追蹤 B,不代表 B 追蹤 A)
related_name='followers', # 反向查詢:誰追蹤了我
blank=True
)
def __str__(self):
return self.username
alice = UserProfile.objects.create(username='alice')
bob = UserProfile.objects.create(username='bob')
# Alice 追蹤 Bob
alice.following.add(bob)
# 查詢 Alice 追蹤了誰
alice.following.all() # <QuerySet [<UserProfile: bob>]>
# 查詢誰追蹤了 Bob
bob.followers.all() # <QuerySet [<UserProfile: alice>]>
小提醒:
symmetrical=False是關鍵參數。預設情況下,ManyToManyField 自我參照時symmetrical=True,表示關係是對稱的(像好友關係)。設為False後,追蹤關係就變成單向的。
關聯關係選擇指南
面對不同的業務需求,如何選擇正確的關聯類型?以下是一張快速對照表:
| 業務場景 | 問自己的問題 | 關聯類型 | 範例 |
|---|---|---|---|
| 一筆對一筆 | A 只能有一個 B,B 也只能有一個 A? | OneToOneField | User ↔ Profile |
| 一筆對多筆 | A 可以有多個 B,但 B 只屬於一個 A? | ForeignKey | Author → Posts |
| 多筆對多筆 | A 可以有多個 B,B 也可以有多個 A? | ManyToManyField | Posts ↔ Tags |
| 多對多 + 額外資訊 | 兩者的關聯本身需要儲存資料? | ManyToManyField + through | Student ↔ Course(含成績) |
| 階層或樹狀結構 | 同一類物件之間有上下級關係? | ForeignKey('self') | Employee → Manager |
| 雙向多對多(對稱) | 同一類物件互相關聯且對等? | ManyToManyField('self') | User ↔ Friends |
| 單向多對多(不對稱) | 同一類物件單向關聯? | ManyToManyField('self', symmetrical=False) | User → Following |
設計原則
- 外鍵放在「多」的那一方:Post 有很多 Comment,ForeignKey 放在 Comment 上。
- ManyToManyField 放哪邊都可以,但通常放在語意上比較「主動」的那一方,例如 Post 有 tags,比 Tag 有 posts 更直覺。
- 能用 ForeignKey 就不要用 ManyToManyField,因為多對多會多一張中介表,增加查詢複雜度。
- 善用
related_name,讓正反向查詢的程式碼都清晰可讀。 - 慎選
on_delete,這是保護資料完整性(Data Integrity)的重要防線。
效能提醒:避免 N+1 查詢問題
在使用關聯查詢時,最常遇到的效能陷阱就是 N+1 查詢問題。Django 提供了 select_related 和 prefetch_related 來解決:
# 不好的做法:每次存取 post.author 都會觸發一次 SQL 查詢
posts = Post.objects.all()
for post in posts:
print(post.author.name) # 每次迴圈都執行一次 SQL(N+1 問題)
# 好的做法:使用 select_related(適用於 ForeignKey / OneToOneField)
posts = Post.objects.select_related('author').all()
for post in posts:
print(post.author.name) # 不再額外查詢,一次 JOIN 搞定
# 好的做法:使用 prefetch_related(適用於 ManyToManyField / 反向 ForeignKey)
posts = Post.objects.prefetch_related('tags').all()
for post in posts:
print(post.tags.all()) # 只額外執行一次查詢
| 方法 | 適用關聯 | 原理 |
|---|---|---|
select_related | ForeignKey、OneToOneField | 使用 SQL JOIN,一次查詢取得所有資料 |
prefetch_related | ManyToManyField、反向 ForeignKey | 額外執行一次查詢,在 Python 層面合併 |
總結
Django Models 的三種關聯關係是資料庫設計的基石。回顧本文的核心重點:
- OneToOneField 適用於一對一擴充,如 User 與 Profile,反向查詢回傳單一物件。
- ForeignKey 是最常用的一對多關聯,外鍵放在「多」的那一方,搭配
related_name讓反向查詢更直覺。 - ManyToManyField 處理多對多關係,Django 自動建立中介表,透過
add()、remove()、clear()操作關聯。 - on_delete 是保護資料完整性的關鍵,
CASCADE和PROTECT是最常用的兩個選項。 - through 中介模型 讓你在多對多關係中儲存額外資訊,例如選課的成績和日期。
- 自我參照關係 解決階層結構和社群追蹤等場景,注意
symmetrical參數的設定。 - 善用
select_related和prefetch_related避免 N+1 查詢問題,是寫出高效能 Django 應用的必備技巧。
掌握這些關聯關係後,你就能設計出結構清晰、查詢高效的資料庫架構,為後續的 View 和 Template 開發打下扎實的基礎。