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

更新日期: 2025 年 1 月 7 日

在 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. ForeignKeyuserservice 都是外鍵,分別關聯到 UserService 模型。
  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資料庫索引資料庫層高效 (直接索引查詢)強制防止重複

結論
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 專案具備更好的數據完整性與用戶體驗!

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *