منطق را به پایگاه داده بسپارید
یک خطای رایج این است که همهی ردیفها را به پایتون بیاوریم و آنجا جمع، شمارش یا مقایسه کنیم. پایگاه داده برای همین کارها ساخته شده و دهها برابر سریعتر است. ابزارهای اصلی ORM برای این کار Q، F، annotate و aggregate هستند.
Q: شرطهای «یا» و «نقیض»
from django.db.models import Q
# سفارشهای پرداختشده یا ارسالشده که مشتریشان تهرانی نیست
Order.objects.filter(Q(status="paid") | Q(status="shipped"), ~Q(customer__city="تهران"))
# ساخت پویا از فرم جستوجو
cond = Q()
if q := request.GET.get("q"):
cond &= Q(tracking_code__icontains=q) | Q(customer__full_name__icontains=q)
if city := request.GET.get("city"):
cond &= Q(customer__city=city)
orders = Order.objects.filter(cond)
F: ارجاع به مقدار ستون
from django.db.models import F
Carpet.objects.filter(stock__lt=F("min_stock")) # مقایسهی دو ستون
Carpet.objects.filter(pk=7).update(stock=F("stock") - 1) # کم کردن اتمیک در SQL
Carpet.objects.update(price=F("price") * 110 / 100) # افزایش ۱۰٪ همهی قیمتها
stock=F("stock") - 1 در خود پایگاه داده اجرا میشود؛ پس اگر دو درخواست همزمان برسند، هیچکدام تغییر دیگری را بازنویسی نمیکند (race condition کلاسیک «خواندن، کم کردن، ذخیره» پیش نمیآید).
aggregate و annotate
aggregate یک دیکشنری برای کل QuerySet برمیگرداند؛ annotate به هر ردیف یک ستون محاسبهشده اضافه میکند.
from django.db.models import Avg, Count, Sum, BigIntegerField
from django.db.models.functions import Coalesce
Order.objects.filter(status="paid").aggregate(total=Sum("total"), avg=Avg("total"), n=Count("id"))
# {'total': 8450000000, 'avg': 70416666.6, 'n': 120}
vip = (Customer.objects
.annotate(order_count=Count("orders", distinct=True),
spent=Coalesce(Sum("orders__total"), 0, output_field=BigIntegerField()))
.filter(order_count__gte=3)
.order_by("-spent"))
values و values_list: فقط همان ستونها
Order.objects.values("status").annotate(n=Count("id")) # گروهبندی بر اساس status
Carpet.objects.values_list("code", flat=True) # ['KSH-1200-17', ...]
Carpet.objects.values_list("code", "price", named=True) # namedtuple
ترتیب مهم است: values() قبل از annotate() یعنی GROUP BY؛ بعد از آن یعنی فقط انتخاب ستونها.
نکتههایی که کمتر کسی میداند
- دو
CountیاSumروی دو رابطهی چندتایی مختلف در یک annotate، به دلیل JOIN نتیجه را ضرب میکنند؛ برای Count ازdistinct=Trueو برای Sum از Subquery (درس ۱۴) استفاده کنید. - بعد از
obj.stock = F("stock") - 1; obj.save()مقدارobj.stockدر پایتون یک عبارت F است، نه عدد؛ قبل از استفادهobj.refresh_from_db(fields=["stock"])بزنید. Sumروی مجموعهی خالیNoneبرمیگرداند نه صفر؛ Coalesce یا در Django 4.0 به بعد پارامترdefault=0خود aggregate را به کار ببرید:Sum("total", default=0).TruncMonthماه میلادی را برمیگرداند؛ برای گزارش ماهانهی شمسی باید بازهی هر ماه شمسی را با jdatetime حساب کنید و باcreated_at__rangeفیلتر کنید (پروژهی پایانی).- ordering پیشفرض Meta روی GROUP BY اثر ندارد (از Django 3.1)، اما
order_by()صریح شما وارد GROUP BY میشود و ممکن است گروهها را بشکند؛ اگر گروهبندی عجیب شد،order_by()خالی بزنید.