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

کلاس Meta: ordering، indexes، constraints و نام‌های فارسی

Meta؛ جایی که مدل درباره‌ی خودش حرف می‌زند

کلاس درونی Meta رفتار کلی مدل را تعیین می‌کند: نام فارسی در ادمین، ترتیب پیش‌فرض، ایندکس‌های ترکیبی و قیدهای پایگاه داده. قیدها (constraints) مهم‌ترین بخش‌اند؛ چون ضمانتی می‌دهند که هیچ باگی در کد پایتون نمی‌تواند دورش بزند.

from django.db import models
from django.db.models import Q


class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "در انتظار پرداخت"
        PAID = "paid", "پرداخت‌شده"
        SHIPPED = "shipped", "ارسال‌شده"
        CANCELLED = "cancelled", "لغوشده"

    customer = models.ForeignKey("orders.Customer", on_delete=models.PROTECT, related_name="orders")
    status = models.CharField(max_length=10, choices=Status, default=Status.PENDING)
    total = models.BigIntegerField(default=0)
    discount = models.BigIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = "سفارش"
        verbose_name_plural = "سفارش‌ها"
        ordering = ["-created_at"]
        get_latest_by = "created_at"
        indexes = [
            models.Index(fields=["status", "-created_at"], name="order_status_created_idx"),
        ]
        constraints = [
            models.CheckConstraint(condition=Q(total__gte=0), name="order_total_gte_0"),
            models.CheckConstraint(condition=Q(discount__lte=models.F("total")),
                                   name="order_discount_lte_total",
                                   violation_error_message="تخفیف نمی‌تواند از مبلغ سفارش بیشتر باشد."),
            models.CheckConstraint(condition=Q(status__in=["pending", "paid", "shipped", "cancelled"]),
                                   name="order_status_valid"),
            # هر مشتری فقط یک سفارش «در انتظار» داشته باشد (سبد خرید باز)
            models.UniqueConstraint(fields=["customer"], condition=Q(status="pending"),
                                    name="one_pending_order_per_customer"),
        ]

ordering؛ راحت اما نه رایگان

ordering روی همه‌ی کوئری‌ها اعمال می‌شود، حتی جایی که ترتیب مهم نیست. روی جدول‌های بزرگ این یعنی مرتب‌سازی اضافه در هر کوئری. اگر ترتیب پیش‌فرض دارید، ایندکس متناسب با آن هم بسازید (مثل ایندکس بالا روی -created_at) یا ordering را حذف کنید و فقط در جای لازم order_by() بنویسید.

ایندکس درست

ایندکس ترکیبی ["status", "-created_at"] دقیقاً برای کوئری‌ای مثل «سفارش‌های در انتظار، جدیدترین اول» ساخته شده است. ترتیب ستون‌ها مهم است: ایندکس از چپ استفاده می‌شود؛ پس کوئری فقط روی created_at از آن سود نمی‌برد.

چرا قید، وقتی اعتبارسنجی فرم داریم؟

فرم فقط یکی از درهای ورود داده است. API، دستور مدیریتی، اسکریپت واردکردن اکسل، QuerySet.update() و حتی یک همکار که مستقیم با psql کار می‌کند، همه فرم را دور می‌زنند. قید پایگاه داده آخرین خط دفاع است و هزینه‌ی تقریباً صفری دارد. قید one_pending_order_per_customer بالا مثلاً مشکل کلاسیک «دو سبد خرید هم‌زمان» را که از دو تب مرورگر ساخته می‌شود، بدون هیچ قفل و منطق پایتونی حل می‌کند.

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

  • از Django 5.1 پارامتر CheckConstraint نامش condition است؛ check قدیمی منسوخ شده. اگر روی 5.0 هستید همان check= را بنویسید.
  • قیدها از Django 4.1 در full_clean() هم بررسی می‌شوند؛ یعنی ModelForm و ادمین پیام خطای violation_error_message را به‌جای IntegrityError نشان می‌دهند.
  • UniqueConstraint شرطی (با condition) روی PostgreSQL و SQLite کار می‌کند اما MySQL آن را نادیده می‌گیرد؛ اگر پایگاه داده‌تان MySQL است به این قید تکیه نکنید.
  • بدون verbose_name_plural ادمین جنگو فقط یک «s» لاتین به نام فارسی می‌چسباند و «سفارشs» می‌بینید.
  • نام ایندکس و قید حداکثر ۳۰ کاراکتر (برای سازگاری با Oracle) و در کل پروژه یکتاست؛ از الگوی app_model_field_idx استفاده کنید تا تداخل پیش نیاید.

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