دادهها تنها زندگی نمیکنند
یک مشتری چند سفارش دارد (یکبهچند)، هر کاربر یک پروفایل مشتری دارد (یکبهیک) و هر سفارش چند طرح فرش با تعداد و قیمت مشخص دارد (چندبهچند با اطلاعات اضافه). جنگو برای هر کدام فیلد مخصوص دارد.
# 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، گزینههای فرم و ادمین را محدود میکند؛ بدون نوشتن حتی یک خط کد در فرم.