فصل ۲: مدل‌ها و ORM — طراحی داده‌ی درست

متدهای مدل، save و سیگنال‌ها: هر منطق کجا بنشیند؟

مدل‌های چاق، Viewهای لاغر — با احتیاط

منطقی که به «خود داده» مربوط است (کد رهگیری، محاسبه‌ی جمع، قابل‌پرداخت بودن) جایش در مدل است، نه در ده View مختلف. جنگو چند نقطه‌ی استاندارد برای این کار دارد:

import secrets

from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils import timezone


class Order(models.Model):
    # ... فیلدهای درس قبل ...
    delivery_date = models.DateField(null=True, blank=True)

    def __str__(self):
        return f"سفارش {self.tracking_code or self.pk}"

    def get_absolute_url(self):
        return reverse("orders:detail", kwargs={"pk": self.pk})

    @property
    def is_payable(self):
        return self.status == self.Status.PENDING and self.total > 0

    def clean(self):
        if self.delivery_date and self.delivery_date < timezone.localdate():
            raise ValidationError({"delivery_date": "تاریخ تحویل نمی‌تواند در گذشته باشد."})

    def save(self, *args, **kwargs):
        if not self.tracking_code:
            self.tracking_code = "KSH" + secrets.token_hex(4).upper()
            update_fields = kwargs.get("update_fields")
            if update_fields is not None:
                kwargs["update_fields"] = {"tracking_code", *update_fields}
        super().save(*args, **kwargs)
نقطهکِی اجرا می‌شودمناسب برای
__str__نمایش در ادمین، shell، قالبمتن خوانا و کوتاه
get_absolute_urlredirect(obj)، دکمه‌ی «View on site» ادمینآدرس صفحه‌ی جزئیات
clean()فقط در full_clean()؛ یعنی ModelForm و ادمیناعتبارسنجی بین چند فیلد
save()هر ذخیره‌ی تکیمقداردهی خودکار فیلدها
سیگنالقبل/بعد از save و deleteواکنش اپ «دیگر» به یک رویداد

سیگنال‌ها و وقتی نباید سراغشان رفت

# apps/notifications/signals.py
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from apps.orders.models import Order
from .tasks import send_order_sms

@receiver(post_save, sender=Order, dispatch_uid="order_created_sms")
def order_created(sender, instance, created, **kwargs):
    if created:
        transaction.on_commit(lambda: send_order_sms.delay(instance.pk))

# apps/notifications/apps.py
class NotificationsConfig(AppConfig):
    name = "apps.notifications"
    def ready(self):
        from . import signals  # noqa: F401  ثبت گیرنده‌ها

سیگنال منطق را «پنهان» می‌کند: کسی که order.save() را می‌خواند نمی‌فهمد پیامکی هم فرستاده می‌شود. قاعده: اگر منطق در همان اپ است، یک تابع سرویس صریح (مثل place_order()) بنویسید. سیگنال را برای جداسازی اپ‌ها نگه دارید؛ مثلاً اپ اعلان‌ها که نباید اپ سفارش از وجودش باخبر باشد.

نکته‌هایی که کمتر کسی می‌داند

  • save() به‌طور خودکار full_clean() را صدا نمی‌زند؛ پس clean() شما در shell، API و اسکریپت‌ها اجرا نمی‌شود مگر خودتان صدایش کنید.
  • QuerySet.update() و bulk_create() نه save() را صدا می‌زنند نه سیگنال‌های pre_save/post_save را؛ اما QuerySet.delete() سیگنال‌های حذف را برای تک‌تک اشیا می‌فرستد.
  • اگر در save فیلدی را خودکار مقدار می‌دهید، حتماً آن را به update_fields اضافه کنید (مثل کد بالا)؛ وگرنه فراخوانی save(update_fields=[...]) مقدار جدید را ذخیره نمی‌کند.
  • بدون dispatch_uid، اگر ماژول سیگنال دو بار import شود گیرنده دو بار ثبت می‌شود و مشتری دو پیامک می‌گیرد.
  • کار خارجی (پیامک، ایمیل، وب‌هوک) را همیشه در transaction.on_commit بگذارید؛ وگرنه ممکن است پیامک برود و تراکنش rollback شود.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.