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

روابط: ForeignKey و on_delete، OneToOne، ManyToMany و through

داده‌ها تنها زندگی نمی‌کنند

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

# apps/orders/models.py
from django.conf import settings
from django.db import models


class Customer(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
                                related_name="customer")
    full_name = models.CharField("نام کامل", max_length=120)
    city = models.CharField("شهر", max_length=50, default="کاشان")


class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name="orders")
    carpets = models.ManyToManyField("catalog.Carpet", through="OrderItem", related_name="orders")
    tracking_code = models.CharField(max_length=12, unique=True, blank=True)
    status = models.CharField(max_length=10, default="pending")
    total = models.PositiveBigIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)


class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
    carpet = models.ForeignKey("catalog.Carpet", on_delete=models.PROTECT)
    quantity = models.PositiveSmallIntegerField(default=1)
    unit_price = models.PositiveBigIntegerField()   # قیمت لحظه‌ی خرید؛ نه قیمت فعلی طرح

on_delete: وقتی والد حذف می‌شود

گزینهرفتارمثال مناسب
CASCADEفرزندان هم حذف می‌شونداقلام یک سفارش
PROTECTحذف والد با ProtectedError متوقف می‌شودمشتری‌ای که سفارش دارد
RESTRICTمثل PROTECT، اما اگر والد از مسیر CASCADE دیگری حذف شود اجازه می‌دهدساختارهای تودرتو
SET_NULLستون NULL می‌شود (نیاز به null=True)نویسنده‌ی یک مقاله
SET_DEFAULT / SET(...)مقدار پیش‌فرض یا حاصل یک تابعانتقال به «کاربر حذف‌شده»
DO_NOTHINGجنگو کاری نمی‌کند؛ پایگاه داده تصمیم می‌گیردتقریباً هیچ‌وقت

کار با روابط

customer = request.user.customer             # OneToOne معکوس
customer.orders.filter(status="pending")      # related_name
order.items.select_related("carpet")          # اقلام با طرح‌ها
order.carpets.add(carpet, through_defaults={"quantity": 2, "unit_price": carpet.price})
Order.objects.filter(customer__city="کاشان", items__carpet__density=1200).distinct()

جدول واسط OrderItem دلیل مهمی دارد: قیمت فرش فردا عوض می‌شود، اما فاکتور دیروز نباید عوض شود. هر رابطه‌ی چندبه‌چندی که «اطلاعات خودش» را دارد باید through داشته باشد.

OneToOne یا ForeignKey با unique؟

از نظر پایگاه داده هر دو یک ستون یکتا می‌سازند، اما رفتار پایتونی فرق دارد: در OneToOne دسترسی معکوس (user.customer) یک شیء برمی‌گرداند و اگر وجود نداشته باشد استثنای RelatedObjectDoesNotExist می‌دهد؛ در ForeignKey یک Manager برمی‌گردد. برای «پروفایل» و «اطلاعات تکمیلی» همیشه OneToOne را انتخاب کنید و در View با hasattr(request.user, "customer") یا getattr نبودنش را مدیریت کنید، نه با try بی‌انتها.

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

  • اشاره با رشته ("catalog.Carpet") مشکل import حلقوی بین اپ‌ها را حل می‌کند؛ برای اشاره به خود مدل هم "self" بنویسید.
  • related_name="+" رابطه‌ی معکوس را کاملاً غیرفعال می‌کند؛ برای فیلدهایی مثل created_by که هرگز از سمت کاربر پیمایش نمی‌شوند.
  • ForeignKey به‌صورت خودکار ایندکس دارد؛ ایندکس دستی روی آن لازم نیست مگر ایندکس ترکیبی بخواهید.
  • فیلتر روی رابطه‌ی چندتایی (items__carpet__...) ممکن است ردیف تکراری بدهد؛ .distinct() را فراموش نکنید.
  • limit_choices_to={"status": "active"} روی ForeignKey، گزینه‌های فرم و ادمین را محدود می‌کند؛ بدون نوشتن حتی یک خط کد در فرم.

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