Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

Django 如何限制同一使用者只能對同一服務留言一次?

最後更新:2025年1月7日Python

本文為 留言評分功能 系列教學,第 9 篇:

  1. 使用 Django + Tailwind + Alpine.js 實作「五顆星評分」功能教學
  2. Django validators 驗證器完整教學
  3. Django PositiveSmallIntegerField 新手指南
  4. 在 esbuild 專案中整合 Alpine.js 的完整指南
  5. 使用 Alpine.js 建立星星評分表單 — 新手指南
  6. 深入理解 Alpine.js 中的 template 標籤使用指南
  7. Django 網站如何新增「星星評分」功能 — 後端接收邏輯
  8. Django 表單:如何讓使用者選擇性提交星星評分與留言
  9. Django 如何限制同一使用者只能對同一服務留言一次? 👈 所在位置
  10. Django 留言軟刪除邏輯|程式碼解析
  11. Django 計算評分摘要教學 — 使用 ORM 進行星等統計
  12. Python Django 使用 annotate、aggregate 統計教學
  13. 使用 Alpine.js 實作星級評分分佈 – 詳細教學
  14. Alpine.js 與 Tailwind CSS 動態樣式解析:為什麼有些樣式無法生效?
  15. 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")

這段程式碼的行為:

  1. ForeignKey:user 和 service 都是外鍵,分別關聯到 User 和 Service 模型。
  2. 現狀:這樣的設計允許 一位使用者針對同一個服務多次留言。

問題:

  • 一位使用者可以針對同一個服務留下多個留言與評分。
  • 這可能會導致評分失真,降低數據的公正性。

解決方案:使用 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資料庫索引資料庫層高效 (直接索引查詢)強制防止重複
檢查方式if exists()
層級應用程式層
效能中等 (需要額外查詢)
可靠性競爭條件風險
檢查方式資料庫索引
層級資料庫層
效能高效 (直接索引查詢)
可靠性強制防止重複

結論:
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?

  1. 定義唯一性限制 (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')
        ]
  1. 執行資料庫遷移:
python manage.py makemigrations
python manage.py migrate
  1. 捕捉錯誤 (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 專案具備更好的數據完整性與用戶體驗!

目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

Python

目錄

  • 問題說明
  • 解決方案:使用 UniqueConstraint 限制留言次數
  • 為什麼使用 UniqueConstraint?
  • 什麼是 UniqueConstraint?
  • 為什麼使用 UniqueConstraint?
  • 效能更佳
  • 自動觸發例外,便於錯誤處理
  • 什麼情況會觸發 IntegrityError?
  • 如何正確使用 UniqueConstraint?
  • 務應用!😊
  • 總結