Django 收藏功能的實現:使用 ManyToMany 關係與自定義中介模型
更新日期: 2024 年 12 月 1 日
本文為 Django 會員系統建立教學,第 8 篇:
- 設計登入與註冊功能的基礎路由與頁面配置 👈 所在位置
- Django 使用者密碼加密方式詳解
- 使用 Django 內建功能實現使用者註冊與登入
- 使用 Django 實現安全登出功能:完整指南
- Django 使用者登入與資料操作的最佳實踐
- Django: 添加公開留言與履歷列表功能
- 使用 Alpine.js 和 Django 動態管理留言按鈕啟用狀態
- Django 收藏功能的實現:使用 ManyToMany 關係與自定義中介模型
建議閱讀本文前,先閱讀完 Django 與前端框架教學 系列文
在本教學中,我們將介紹如何實現收藏功能,並通過 Django 的 ManyToManyField
和自定義中介模型,完成對收藏狀態的添加與刪除功能。
同時,我們還將優化視圖與模板,實現動態樣式切換,提升用戶體驗。
基礎關係回顧
在 Django 中,模型之間的關係主要包括:
- 一對多關係:一個模型的實例可以關聯到多個其他模型的實例。
- 多對多關係:兩個模型的實例可以互相多對多關聯。
一對多關係
範例:User 與 Resume
在一對多關係中,一個 User 可以有多個 Resume,但每個 Resume 只能屬於一個 User。
User ===== has many ====> Resumes
<------------------ resumes.user_id (ForeignKey)
範例:Resume 與 Comment
同樣,一個 Resume 可以有多個 Comment,但每個 Comment 只能屬於一個 Resume。
Resume ===== has many ====> Comments
<------------------ comments.resume_id (ForeignKey)
多對多關係
當兩個模型的關係是「雙向多對多」時,例如:
- 一個 User 可以收藏多個 Resume。
- 一個 Resume 也可以被多個 User 收藏。
這種情況需要用到多對多關係。
多對多的數據庫設計
在多對多關係中,通常需要引入一個「第三個表格」,用來記錄兩個模型的關聯。
這個表格被稱為連接表(Join Table)。
多對多結構
以下是多對多關係的結構示意:
User ====== has many ======> Resume
User <--------------------> Join Table <--------------------> Resume
連接表會記錄兩邊的關聯,例如:
- user_id:指向 User 的主鍵。
- resume_id:指向 Resume 的主鍵。
設置收藏模型與數據庫
新增中介模型
定義一個中介模型 FavoriteResume
,用於管理 User
和 Resume
的多對多關係:
檔案位置:resumes/models.py
from django.db import models
from django.contrib.auth.models import User
class Resume(models.Model):
title = models.CharField(max_length=100)
skill = models.CharField(max_length=300, null=True)
location = models.CharField(max_length=200, null=True)
content = models.TextField(null=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) # 履歷擁有者
favorite_users = models.ManyToManyField(
User,
related_name="favorited_resumes", # 反向查詢名稱
through="FavoriteResume", # 中介模型
through_fields=("resume", "user"), # 定義中介模型的關聯字段順序
)
def __str__(self):
return f"{self.title} ({self.skill})"
class FavoriteResume(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # 收藏者
resume = models.ForeignKey("Resume", on_delete=models.CASCADE) # 被收藏的履歷
created_at = models.DateTimeField(auto_now_add=True) # 收藏時間
多對多關係的模型設計與說明
在 Django 中,多對多關係 是指一個模型的實例可以與另一個模型的多個實例相關聯,反之亦然。以下代碼設計展示了如何使用 ManyToManyField
和中介模型實現履歷的收藏功能。
模型代碼與說明
from django.db import models
from django.contrib.auth.models import User
class Resume(models.Model):
title = models.CharField(max_length=100) # 履歷標題
skill = models.CharField(max_length=300, null=True) # 技能描述
location = models.CharField(max_length=200, null=True) # 工作地點
content = models.TextField(null=True) # 詳細內容
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) # 履歷的擁有者
# 多對多關聯字段,使用中介模型 "FavoriteResume"
favorite_users = models.ManyToManyField(
User,
related_name="favorited_resumes", # 反向查詢名稱
through="FavoriteResume", # 指定中介模型
through_fields=("resume", "user"), # 定義中介模型的字段順序
)
def __str__(self):
return f"{self.title} ({self.skill})"
class FavoriteResume(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # 收藏者
resume = models.ForeignKey("Resume", on_delete=models.CASCADE) # 被收藏的履歷
created_at = models.DateTimeField(auto_now_add=True) # 收藏的時間
關鍵概念解析
在本設計中,使用了 FavoriteResume
作為中介模型,將 Resume
和 User
模型關聯起來,並記錄額外信息。
- 中介模型
FavoriteResume
的結構:- 包含兩個外鍵,分別指向
User
和Resume
。 - 額外字段
created_at
用於記錄用戶收藏履歷的時間。 - 中介模型不僅儲存關係,還能儲存更多信息,這是直接使用
ManyToManyField
所無法實現的。
- 包含兩個外鍵,分別指向
ManyToManyField
與 through
關鍵字的解析
ManyToManyField
的功能:- 在
Resume
模型中定義favorite_users
字段,表示某份履歷被哪些用戶收藏。 - 使用
through="FavoriteResume"
指定中介模型。
- 在
through
的作用:- 通知 Django 使用
FavoriteResume
表作為多對多關係的橋樑。 through_fields
指定中介模型字段的關聯順序:("resume", "user")
。
- 通知 Django 使用
- 為什麼
User
模型中未設置through
?- 一般情況下,若要建立兩張表的多對多關係,兩張表都需要設置
through
。 - 然而,這裡通過在
Resume
模型中設置related_name="favorited_resumes"
,讓User
模型能以反向查詢的方式實現關聯,避免了在內建User
模型中額外設置。
- 一般情況下,若要建立兩張表的多對多關係,兩張表都需要設置
related_name
的功能
- 反向查詢的關聯:
resume.favorite_users.all()
:從履歷的角度查詢所有收藏該履歷的用戶。user.favorited_resumes.all()
:從用戶的角度查詢用戶收藏的所有履歷。
- 查詢的簡化:
- 通過
related_name
直接建立反向查詢名稱,無需在每次查詢時明確指定字段或表。
- 通過
設計後的功能舉例
- 正向查詢:
resume = Resume.objects.get(id=1)
users = resume.favorite_users.all() # 獲取收藏該履歷的所有用戶
- 反向查詢:
user = User.objects.get(id=1)
resumes = user.favorited_resumes.all() # 獲取該用戶收藏的所有履歷
- 新增收藏:
user = User.objects.get(id=1)
resume = Resume.objects.get(id=1)
FavoriteResume.objects.create(user=user, resume=resume) # 手動創建記錄
- 刪除收藏:
FavoriteResume.objects.filter(user=user, resume=resume).delete()
遷移數據庫
執行以下命令,生成並應用遷移:
python manage.py makemigrations
python manage.py migrate
遷移與數據庫操作
- 執行遷移命令:
- 在新增模型後,需要執行以下命令生成數據庫表:
python manage.py makemigrations
python manage.py migrate
- 為什麼
ManyToManyField
不會直接再 Resumes 資料庫中新增字段?ManyToManyField
通常不會在主表中新增字段,而是通過第三張表(如FavoriteResume
)存儲關聯數據。- 這裡的
favorite_users
實際上是一個代理,Django 使用它來操作FavoriteResume
表。
設置收藏功能的基礎路徑與視圖
新增 URL 路徑
在 resumes/urls.py
中,為收藏功能新增路徑 like
:
from django.urls import path
from . import views
app_name = "resumes"
urlpatterns = [
# 其他路徑...
path("<int:id>/like", views.like, name="like"), # 新增收藏路徑
]
定義 like
函數
在 resumes/views.py
中,定義處理收藏的邏輯。
使用 @require_POST
確保只接受 POST 請求,並使用 @login_required
確保用戶已登入:
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.shortcuts import get_object_or_404, redirect
from .models import Resume, FavoriteResume
@require_POST
@login_required
def like(request, id):
resume = get_object_or_404(Resume, id=id)
# 檢查是否已收藏
favorite = resume.favorite_users.filter(id=request.user.id).first()
if favorite:
# 已收藏:刪除收藏
FavoriteResume.objects.get(user=request.user, resume=resume).delete()
else:
# 未收藏:新增收藏
FavoriteResume(user=request.user, resume=resume).save()
return redirect("resumes:show", id=resume.id)
在收藏功能中,我們需要檢查當前用戶是否已經收藏了特定的履歷:
favorite = resume.favorite_users.filter(id=request.user.id).first()
- 含義:
- 使用
filter(id=request.user.id)
從resume.favorite_users
的查詢集中,找到當前用戶的收藏記錄。 - 如果有結果,
first()
返回第一個記錄(即FavoriteResume
中的一行數據)。 - 如果查詢集為空,
first()
返回None
,不會引發錯誤。
- 使用
- 後續邏輯:
- 如果
favorite
不為空,則說明用戶已經收藏該履歷,此時刪除收藏記錄。 - 如果
favorite
為None
,則新增一條收藏記錄。
- 如果
補充說明:為什麼使用 .first()
而不是 [0]
filter()
方法的返回值:
- Django 的
filter()
方法會返回一個查詢集(QuerySet)。 - 這是一個類似列表的對象,包含所有符合過濾條件的記錄。
- 即使查詢集結果為空,也不會引發錯誤,因為空查詢集仍然是合法的對象。
.first()
的作用:
first()
方法會從查詢集中返回第一個結果,如果查詢集為空,則返回None
。- 適合用於檢查「是否有結果」的情況。
- 使用
.first()
的安全性:- 若查詢集為空,
first()
返回None
,程式可以繼續執行。 - 避免了直接使用索引(如
[0]
)可能導致的IndexError
。
- 若查詢集為空,
為什麼不能使用 [0]
:
- 當查詢集為空時,試圖訪問
[0]
會觸發IndexError
,導致程式崩潰。 - 與
.first()
不同,[0]
不會自動處理空查詢集的情況,因此需要額外檢查是否有結果。
效果比較
方法 | 行為描述 | 安全性 |
---|---|---|
.first() | 返回第一個結果,若查詢集為空則返回 None | 安全,無需額外檢查空查詢集 |
[0] | 返回第一個結果,若查詢集為空則引發 IndexError | 不安全,需要額外檢查 |
因此,使用 .first()
是更安全且簡潔的選擇,適合處理可能為空的查詢集情況。
模板邏輯與樣式更新
傳遞收藏狀態到模板
修改 show
函數,將當前用戶的收藏狀態傳遞到模板:
from django.shortcuts import render
def show(request, id):
resume = get_object_or_404(Resume, id=id)
favorited = resume.favorite_users.filter(id=request.user.id).first() # 收藏狀態
comments = resume.comment_set.all()
return render(
request,
"resumes/show.html",
{
"resume": resume,
"comments": comments,
"favorited": favorited, # 傳遞收藏狀態
},
)
引入 SVG 資源
搜尋與準備 SVG 圖檔
我們將為收藏功能設計兩種愛心圖案:
- 實心愛心:代表「已收藏」狀態。
- 空心愛心:代表「未收藏」狀態。
將下載的 heart-on.svg
和 heart-off.svg
存放於 static/svg
資料夾中。
認識 SVG
SVG(Scalable Vector Graphics)是一種基於 XML 的圖形格式。它本質上是文字檔案,透過特殊的 HTML 組成圖形,具有以下特點:
- 支援縮放而不失真。
- 可直接內嵌在 HTML 或通過
<img>
引用。
更新 show.html
在 resumes/templates/resumes/show.html
中,添加收藏按鈕並顯示收藏狀態:
<form method="POST" action="{% url 'resumes:like' resume.id %}">
{% csrf_token %}
<button class="btn btn-sm">
{% if favorited %}
<img src="/static/svg/heart-on.svg" alt="已收藏" />
{% else %}
<img src="/static/svg/heart-off.svg" alt="未收藏" />
{% endif %}
</button>
</form>
代碼解析
method="post"
:設置表單為 POST 請求,符合收藏功能的需求。{% csrf_token %}
:添加 CSRF 保護。action="{% url 'resumes:like' resume.id %}"
:設置表單提交的路徑。
小結
- 設置收藏功能的路由與視圖:確保使用 POST 請求,並檢查用戶是否登入。
- 使用自定義中介模型:提升關聯數據的靈活性與可擴展性。
- 優化模板交互:根據收藏狀態動態切換按鈕樣式,提供更好的用戶體驗。
通過以上步驟,我們成功實現了 Django 中的收藏功能,並運用了 ManyToManyField
與中介模型的進階功能。