Django 如何限制同一使用者只能對同一服務留言一次?
更新日期: 2025 年 1 月 7 日
本文為 留言評分功能 系列教學,第 9 篇:
- 使用 Django + Tailwind + Alpine.js 實作「五顆星評分」功能教學
- Django validators 驗證器完整教學
- Django PositiveSmallIntegerField 新手指南
- 在 esbuild 專案中整合 Alpine.js 的完整指南
- 使用 Alpine.js 建立星星評分表單 — 新手指南
- 深入理解 Alpine.js 中的 template 標籤使用指南
- Django 網站如何新增「星星評分」功能 — 後端接收邏輯
- Django 表單:如何讓使用者選擇性提交星星評分與留言
- Django 如何限制同一使用者只能對同一服務留言一次? 👈 所在位置
- Django 留言軟刪除邏輯|程式碼解析
- Django 計算評分摘要教學 — 使用 ORM 進行星等統計
- Python Django 使用 annotate、aggregate 統計教學
- 使用 Alpine.js 實作星級評分分佈 – 詳細教學
- Alpine.js 與 Tailwind CSS 動態樣式解析:為什麼有些樣式無法生效?
- Django 中使用 annotate() 與 Avg() 進行平均評分計算
在 Django 網站中,如果允許同一位使用者針對同一個服務多次留言與評分,可能會導致評價數據失真,進而影響系統的公正性與使用者體驗。
為了解決這個問題,我們可以透過 Django 的 唯一性約束 (UniqueConstraint),確保「同一位使用者只能對同一服務留言一次」,同時仍允許針對不同服務留下評價。
問題說明
原始的模型設計
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name="comments")
這段程式碼的行為:
- ForeignKey:
user
和service
都是外鍵,分別關聯到User
和Service
模型。 - 現狀:這樣的設計允許 一位使用者針對同一個服務多次留言。
問題:
- 一位使用者可以針對同一個服務留下多個留言與評分。
- 這可能會導致評分失真,降低數據的公正性。
解決方案:使用 UniqueConstraint
限制留言次數
更新後的 models.py
設計
from django.db import models
from django.contrib.auth.models import User
from services.models import Service
from django.core.validators import MaxValueValidator, MinValueValidator
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name="comments")
rating = models.PositiveSmallIntegerField(
null=True,
blank=True,
validators=[MinValueValidator(1), MaxValueValidator(5)],
)
content = models.TextField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
deleted_at = models.DateTimeField(null=True, blank=True)
is_deleted = models.BooleanField(default=False)
# ✅ 使用 UniqueConstraint 限制同一用戶只能留言一次
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'service'], name='unique_user_service_comment')
]
def __str__(self):
return self.content[:20]
為什麼使用 UniqueConstraint
?
在 Django 中,為了防止同一位使用者對同一服務重複提交留言與評分,使用 UniqueConstraint
是一個更安全且高效的選擇。
這裡將更詳細地解釋,為什麼在 Django 中應使用 UniqueConstraint
來處理資料唯一性限制,而不是僅依賴程式邏輯檢查。
什麼是 UniqueConstraint
?
UniqueConstraint
是 Django 中的 資料庫層級 限制,用於確保資料庫中的某些欄位組合是唯一的,防止重複紀錄出現。
例子:
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name="comments")
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'service'], name='unique_user_service_comment')
]
這段程式碼限制了 同一位使用者 (user
) 針對 同一服務 (service
) 只能提交一筆評論。
為什麼使用 UniqueConstraint
?
以下是使用 UniqueConstraint
的三個主要原因,並附上更詳細的技術解釋與範例。
資料庫層級的限制,更安全可靠
資料庫層級限制:
UniqueConstraint
在 資料庫層級 強制限制數據唯一性。- 這表示即使程式邏輯有錯誤或遺漏,也無法將重複的數據存入資料庫。
程式邏輯的風險:
若僅依賴程式碼檢查重複紀錄(例如使用 if
判斷),仍可能出現競爭條件 (Race Condition)。
範例 (程式邏輯檢查的缺陷):
# 不安全的方式 (僅透過程式邏輯檢查)
if not Comment.objects.filter(user=request.user, service=service).exists():
Comment.objects.create(user=request.user, service=service, content="Great!")
問題:
- 如果多個使用者同時提交表單,可能導致兩筆重複的資料被儲存,因為
exists()
檢查與create()
之間存在競爭條件。
範例 (資料庫層級限制 – 更安全):
try:
Comment.objects.create(user=request.user, service=service, content="Great!")
except IntegrityError:
print("這位使用者已經對該服務提交過評論")
優勢:
UniqueConstraint
直接在資料庫層阻止重複紀錄寫入,即使程式有競爭條件也能保證數據一致性。
效能更佳
原因:
UniqueConstraint
在 資料庫層 透過索引 (index) 來檢查數據唯一性,速度更快。- 程式邏輯檢查(如
if exists()
)則需要從應用程式層面查詢數據庫,效能較低且存在額外的查詢負擔。
比較:
方法 | 檢查方式 | 層級 | 效能 | 可靠性 |
---|---|---|---|---|
程式邏輯檢查 | if exists() | 應用程式層 | 中等 (需要額外查詢) | 競爭條件風險 |
UniqueConstraint | 資料庫索引 | 資料庫層 | 高效 (直接索引查詢) | 強制防止重複 |
結論:UniqueConstraint
因為直接使用索引,在大規模資料庫下的查詢效能遠優於程式邏輯檢查。
自動觸發例外,便於錯誤處理
使用 UniqueConstraint
,Django 會在違反唯一性約束時自動觸發 IntegrityError
例外 (Exception)。這讓開發者能更方便地捕捉與處理錯誤。
範例:捕捉 IntegrityError
from django.db import IntegrityError
try:
Comment.objects.create(user=request.user, service=service, content="Good!")
except IntegrityError:
print("此用戶已經針對此服務提交過評論。")
如何觸發 IntegrityError
?
- 當你試圖插入一條 違反唯一性約束 的記錄時,自動觸發。
- 這是由於資料庫層在檢查索引時自動拋出的錯誤。
什麼情況會觸發 IntegrityError
?
| 使用者 | 服務 | 評分 | 狀態 |
|--------|------------|-------|------------|
| Alice | Service A | 5 | ✔️ 可儲存 |
| Alice | Service B | 4 | ✔️ 可儲存 |
| Alice | Service A | 3 | ❌ IntegrityError |
如何正確使用 UniqueConstraint
?
- 定義唯一性限制 (models.py):
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
service = models.ForeignKey(Service, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'service'], name='unique_user_service_comment')
]
- 執行資料庫遷移:
python manage.py makemigrations
python manage.py migrate
- 捕捉錯誤 (views.py):
from django.db import IntegrityError
def add_comment(request, service_id):
try:
comment = Comment.objects.create(
user=request.user,
service_id=service_id,
content="Awesome service!"
)
except IntegrityError:
return HttpResponse("你已經對這個服務留下過評論!")
務應用!😊
總結
- 問題:使用者能對同一服務多次留言,導致數據失真。
- 解決方案:使用 Django 的
UniqueConstraint
在資料庫層強制限制「同一使用者只能對同一服務留言一次」。 - 優勢:
- 確保數據一致性
- 防止重複提交
- 提升程式碼的可維護性
- 執行步驟:
- 更新
models.py
- 執行遷移指令
- 在前端與後端適當處理錯誤訊息
- 更新
這樣的設計能確保你的 Django 專案具備更好的數據完整性與用戶體驗!