Django Signals مکانیزمی برای اطلاع‌رسانی رویدادها بین بخش‌های مستقل پروژه است؛ مثلا بعد از ذخیره یا حذف یک مدل. از Signals زمانی استفاده کنید که می‌خواهید به رخدادهای سطح داده (مثل post_save) واکنش نشان دهید بی‌آن‌که کد اصلی را به وابستگی‌های جانبی آلوده کنید. برای منطق حیاتی و ترتیبی (جریان‌های تجاری پیچیده)، انجام کارهای سنگین یا نیاز به کنترل دقیق زمان و نتیجه، به جای Signals از سرویس‌ها، Celery یا فراخوانی صریح توابع استفاده کنید.

Django Signals دقیقا چیست؟

Signals یک الگوی publish/subscribe درون جنگو است: sender (فرستنده) رویداد را منتشر می‌کند و receiver (گیرنده) مشترک آن رویداد است. نمونه‌های پرکاربرد شامل pre_save، post_save، pre_delete، post_delete، m2m_changed، post_migrate و request_started/finished است. هر receiver امضای مشخصی دارد و می‌تواند پارامترهای اضافی (مثل instance، created، update_fields) را دریافت کند.

چه زمانی از Signals استفاده کنیم؟

  • وقتی رویدادی در لایه داده رخ می‌دهد و می‌خواهید واکنش جانبی غیرحیاتی انجام دهید؛ مثل ثبت لاگ، شمارنده‌های کمکی، آپدیت ایندکس جستجو، یا ارسال اعلان.
  • وقتی می‌خواهید بخش‌های دامنه را از یکدیگر جدا نگه دارید و وابستگی معکوس به وجود نیاورید.
  • برای قواعد ساده و idempotent که تحمل اجرای دوباره را دارند و به ترتیب اجرای دقیق میان چند کار وابسته نیستند.

چه زمانی بهتر است Signals استفاده نکنیم؟

  • منطق بحرانی دامنه یا تراکنش‌های چندمرحله‌ای که به ترتیب و نتیجه قطعی نیاز دارند. اینها را در سرویس‌ها یا view/service layer به صورت صریح پیاده‌سازی کنید.
  • کارهای سنگین یا کند (ایمیل‌های انبوه، پردازش فایل، گزارش‌گیری). این کارها را به صف پس‌زمینه (مثل Celery) منتقل کنید و در signal فقط صف‌بندی را انجام دهید.
  • زمان‌بندی دوره‌ای. برای اجرای زمان‌بندی‌شده از ابزار زمان‌بندی یا دستورهای مدیریت‌شده استفاده کنید. راهنمای عملی در آموزش ساخت Command اختصاصی در Django و زمان بندی اجرا.
  • وقتی به ورودی‌های اضافی یا کنترل بازگشتی نیاز دارید. Signals مقدار بازگشتی معناداری برای زنجیره تصمیم‌گیری شما فراهم نمی‌کند.

مثال 1: ارسال ایمیل خوشامد بعد از ساخت کاربر (با اطمینان از commit)

هدف: پس از ساخت کاربر جدید، یک ایمیل خوشامد بفرستیم. نکته مهم: post_save قبل از commit پایگاه‌داده اجرا می‌شود؛ برای جلوگیری از ارسال ایمیل در صورت برگشت تراکنش، از transaction.on_commit استفاده می‌کنیم.

# accounts/signals.py
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db import transaction
from django.core.mail import send_mail

User = get_user_model()

@receiver(post_save, sender=User, dispatch_uid="welcome_email_on_user_created")
def send_welcome_email_on_user_created(sender, instance, created, **kwargs):
    if not created:
        return

    def _send():
        send_mail(
            subject="خوش آمدید",
            message=f"{instance.first_name or ''} عزیز، به سایت ما خوش آمدید.",
            from_email="noreply@example.com",
            recipient_list=[instance.email],
            fail_silently=True,  # در محصول نهایی لاگ مناسب بگذارید
        )

    # اجرای ایمیل پس از نهایی شدن تراکنش
    transaction.on_commit(_send)

ثبت receiver در AppConfig تا اطمینان یابیم همیشه بارگذاری می‌شود:

# accounts/apps.py
from django.apps import AppConfig

class AccountsConfig(AppConfig):
    name = "accounts"

    def ready(self):
        # بارگذاری receivers
        from . import signals  # noqa

و در INSTALLED_APPS از مسیر AppConfig استفاده کنید: “accounts.apps.AccountsConfig”. اگر از Celery استفاده می‌کنید، به جای ارسال مستقیم ایمیل، داخل on_commit، تسک Celery را صف‌بندی کنید.

مثال 2: شمارش امن کامنت‌های تاییدشده در Post

هدف: وقتی کامنت جدیدی تایید شد یا وضعیتش تغییر کرد، فیلد comment_count در Post را به‌روز کنیم. از F expression استفاده می‌کنیم تا در رقابت همزمان دقیق باقی بماند.

# blog/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    comment_count = models.PositiveIntegerField(default=0)

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    body = models.TextField()
    is_approved = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
# blog/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db.models import F
from .models import Comment, Post

@receiver(post_save, sender=Comment, dispatch_uid="adjust_count_on_comment_save")
def adjust_count_on_comment_save(sender, instance, created, update_fields=None, **kwargs):
    # اگر تازه ایجاد شد و تاییدشده است: +1
    if created and instance.is_approved:
        Post.objects.filter(pk=instance.post_id).update(comment_count=F("comment_count") + 1)
        return

    # اگر به‌روزرسانی بوده و فیلد is_approved تغییر کرده:
    # توجه: برای ردیابی تغییر، یا از update_fields استفاده کنید یا مقدار قبلی را ذخیره کنید.
    # فرض: view/service هنگام تایید از update_fields={"is_approved"} استفاده می‌کند.
    if not created and (update_fields is None or "is_approved" in update_fields):
        if instance.is_approved:
            Post.objects.filter(pk=instance.post_id).update(comment_count=F("comment_count") + 1)
        else:
            Post.objects.filter(pk=instance.post_id).update(comment_count=F("comment_count") - 1)

@receiver(post_delete, sender=Comment, dispatch_uid="adjust_count_on_comment_delete")
def adjust_count_on_comment_delete(sender, instance, **kwargs):
    if instance.is_approved:
        Post.objects.filter(pk=instance.post_id).update(comment_count=F("comment_count") - 1)

نکات ایمنی:

اشتباهات رایج و راه‌حل‌ها

  • ثبت نشدن receiver به خاطر محل نادرست import: signals را داخل فایل جدا (signals.py) تعریف کنید و در متد ready کلاس AppConfig ایمپورت کنید. از import در urls.py یا فایل‌های تصادفی خودداری کنید.
  • اجرای دوباره سیگنال در حالت توسعه به خاطر auto-reload: از dispatch_uid یکتا استفاده کنید تا receiver فقط یک بار متصل شود. همچنین منطق side-effect را idempotent طراحی کنید.
  • وابستگی به وضعیت نهایی تراکنش: post_save قبل از commit اجرا می‌شود. هر عملی که به وجود قطعی رکورد وابسته است (ارسال ایمیل، ارسال پیام به سرویس دیگر) را داخل transaction.on_commit قرار دهید.
  • حلقه بی‌نهایت یا اجرای تکراری: اگر در receiver دوباره همان مدل را save می‌کنید، شرط‌گذاری دقیق انجام دهید یا از update() روی queryset استفاده کنید. در به‌روزرسانی‌های جزئی از update_fields بهره ببرید.
  • گیرکردن در کارهای سنگین: پردازش‌های زمان‌بر را در signal انجام ندهید؛ فقط تسک پس‌زمینه را صف‌بندی کنید.
  • مشکل جمع‌آوری زباله (weak references): سیگنال‌ها به طور پیش‌فرض weak هستند. receiver را در سطح ماژول تعریف کنید یا اگر واقعا لازم است، weak=False بدهید. مثال: @receiver(post_save, sender=Foo, weak=False).
  • استفاده از Signals برای منطق وابسته به درخواست: این موارد را در middleware یا view/service layer نگه دارید. Signals برای رویدادهای سطح داده مناسب‌تر است.

راهنمای پیاده‌سازی تمیز (چک‌لیست کوتاه)

  1. برای هر اپ، AppConfig بسازید و در INSTALLED_APPS از مسیر کامل آن استفاده کنید.
  2. receivers را در signals.py قرار دهید و فقط در ready ایمپورت کنید.
  3. برای هر receiver یک dispatch_uid پایدار تعریف کنید.
  4. side-effect ها را idempotent بنویسید؛ اجرای دوباره نباید فاجعه ایجاد کند.
  5. برای کارهای پس از commit از transaction.on_commit استفاده کنید.
  6. اگر نیاز به ترتیب یا نتیجه قطعی دارید، به جای Signals از سرویس‌ها/توابع صریح استفاده کنید.
  7. تست بنویسید: در تست‌ها با ساخت داده، وقوع اثرهای جانبی (مثلا افزایش شمارنده) را بررسی کنید. در صورت نیاز، سیگنال‌ها را موقتا غیرفعال کنید یا از overrideهای مخصوص استفاده کنید.

گزینه‌های جایگزین بر اساس نیاز

  • Override در مدل (save/delete): برای تضمین یکپارچگی داخلی همان مدل و منطق نزدیک به داده. مراقب حلقه‌ها و ترتیب ذخیره باشید.
  • Service Layer/Use Case: وقتی چندین عملیات باید با ترتیب مشخص و مدیریت خطا اجرا شوند.
  • Celery/Background Jobs: برای کارهای کند و قابل تکرار. signal فقط صف‌بندی کند.
  • مدیریت زمان‌بندی: برای اجراهای دوره‌ای از Management Command یا زمان‌بند استفاده کنید؛ راهنما در زمان بندی اجرا با Command اختصاصی جنگو.
  • قیود و تریگرهای پایگاه‌داده: برای قوانین سفت‌وسخت داده. اما قابلیت حمل‌پذیری را در نظر بگیرید.

از کجا ادامه بدهم؟

اگر در مسیر ساخت یک پروژه واقعی با جنگو هستید، مطالعه سیگنال‌ها زمانی مفید است که بخواهید رخدادهای داده‌ای را از منطق نمایش و درخواست جدا کنید. برای تمرین عملی و ساختن اپلیکیشن فروشگاهی گام‌به‌گام می‌توانید از آموزش جنگو رایگان (فروشگاه اینترنتی) + سورس استفاده کنید. همچنین برای سناریوهای رقابتی، بخش‌های مربوط به تراکنش و قفل را از لینک مدیریت تراکنش که بالاتر آوردیم مرور کنید.