Django Models 關聯關係:一對一、一對多、多對多完全解析 | Django 教學

2026/05/29 2026/05/25
Django Models 關聯關係:一對一、一對多、多對多完全解析 | Django 教學

DjangoORM(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,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 選項詳解

在定義 ForeignKeyOneToOneField 時,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'
    )

最佳實踐:優先考慮 CASCADEPROTECT。如果不確定該用哪個,問自己一個問題:「刪除父物件時,子物件是否還有存在的意義?」有的話用 PROTECTSET_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?OneToOneFieldUser ↔ Profile
一筆對多筆A 可以有多個 B,但 B 只屬於一個 A?ForeignKeyAuthor → Posts
多筆對多筆A 可以有多個 B,B 也可以有多個 A?ManyToManyFieldPosts ↔ Tags
多對多 + 額外資訊兩者的關聯本身需要儲存資料?ManyToManyField + throughStudent ↔ Course(含成績)
階層或樹狀結構同一類物件之間有上下級關係?ForeignKey('self')Employee → Manager
雙向多對多(對稱)同一類物件互相關聯且對等?ManyToManyField('self')User ↔ Friends
單向多對多(不對稱)同一類物件單向關聯?ManyToManyField('self', symmetrical=False)User → Following

設計原則

  1. 外鍵放在「多」的那一方:Post 有很多 Comment,ForeignKey 放在 Comment 上。
  2. ManyToManyField 放哪邊都可以,但通常放在語意上比較「主動」的那一方,例如 Post 有 tags,比 Tag 有 posts 更直覺。
  3. 能用 ForeignKey 就不要用 ManyToManyField,因為多對多會多一張中介表,增加查詢複雜度。
  4. 善用 related_name,讓正反向查詢的程式碼都清晰可讀。
  5. 慎選 on_delete,這是保護資料完整性(Data Integrity)的重要防線。

效能提醒:避免 N+1 查詢問題

在使用關聯查詢時,最常遇到的效能陷阱就是 N+1 查詢問題。Django 提供了 select_relatedprefetch_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_relatedForeignKey、OneToOneField使用 SQL JOIN,一次查詢取得所有資料
prefetch_relatedManyToManyField、反向 ForeignKey額外執行一次查詢,在 Python 層面合併

總結

Django Models 的三種關聯關係是資料庫設計的基石。回顧本文的核心重點:

  • OneToOneField 適用於一對一擴充,如 User 與 Profile,反向查詢回傳單一物件。
  • ForeignKey 是最常用的一對多關聯,外鍵放在「多」的那一方,搭配 related_name 讓反向查詢更直覺。
  • ManyToManyField 處理多對多關係,Django 自動建立中介表,透過 add()remove()clear() 操作關聯。
  • on_delete 是保護資料完整性的關鍵,CASCADEPROTECT 是最常用的兩個選項。
  • through 中介模型 讓你在多對多關係中儲存額外資訊,例如選課的成績和日期。
  • 自我參照關係 解決階層結構和社群追蹤等場景,注意 symmetrical 參數的設定。
  • 善用 select_relatedprefetch_related 避免 N+1 查詢問題,是寫出高效能 Django 應用的必備技巧。

掌握這些關聯關係後,你就能設計出結構清晰、查詢高效的資料庫架構,為後續的 View 和 Template 開發打下扎實的基礎。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。