From e894affad76056c118c6943f91bd6f6d3082a632 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 16 Oct 2025 14:04:18 +0300 Subject: [PATCH] Features: 1) Add `with_related` method to managers for optimized prefetching; 2) Add indexes to models for enhanced query performance; 3) Introduce `cached_property` for feedback count and product quantity; 4) Implement `Exists` and `OuterRef` filters for active stock validation in viewsets. Fixes: 1) Simplify rating and feedback count logic by removing caching layers; 2) Refactor price and quantity calculations for improved accuracy and simplicity; 3) Optimize total price and quantity aggregations in orders by leveraging Django ORM tools. Extra: Adjusted import statements, removed redundant cache logic, and cleaned up methods for better readability and maintainability. --- core/managers.py | 5 ++++ core/models.py | 71 +++++++++++++++++++++--------------------------- core/viewsets.py | 23 +++++++++++----- 3 files changed, 52 insertions(+), 47 deletions(-) diff --git a/core/managers.py b/core/managers.py index f120cad7..caf5f934 100644 --- a/core/managers.py +++ b/core/managers.py @@ -84,3 +84,8 @@ class ProductManager(MultilingualManager): stocks__vendor__is_active=True, stocks__quantity__gt=0, ) + + def with_related(self): + return self.select_related("category", "brand").prefetch_related( + "tags", "stocks", "images", "attributes__attribute__group" + ) diff --git a/core/models.py b/core/models.py index 1ab647c6..6b4b0811 100644 --- a/core/models.py +++ b/core/models.py @@ -20,6 +20,7 @@ from django.db.models import ( CharField, DateTimeField, DecimalField, + F, FileField, FloatField, ForeignKey, @@ -31,6 +32,7 @@ from django.db.models import ( OneToOneField, PositiveIntegerField, QuerySet, + Sum, TextField, URLField, ) @@ -38,6 +40,7 @@ from django.db.models.indexes import Index from django.http import Http404 from django.utils import timezone from django.utils.encoding import force_bytes +from django.utils.functional import cached_property from django.utils.http import urlsafe_base64_encode from django.utils.translation import gettext_lazy as _ from django_extensions.db.fields import AutoSlugField @@ -177,6 +180,7 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): # type: ignore [ verbose_name_plural = _("vendors") indexes = [ GinIndex(fields=["authentication"]), + Index(fields=["name"]), ] @@ -512,6 +516,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore max_length=255, help_text=_("provide a clear identifying name for the product"), verbose_name=_("product name"), + db_index=True, ) description = TextField( blank=True, @@ -556,49 +561,37 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore class Meta: verbose_name = _("product") verbose_name_plural = _("products") + indexes = [ + Index(fields=["is_active", "brand", "category"]), + Index(fields=["slug"]), + Index(fields=["sku"]), + ] def __str__(self): return self.name @property def rating(self) -> float: - cache_key = f"product_rating_{self.pk}" - rating = cache.get(cache_key) - if rating is None: - feedbacks = Feedback.objects.filter(order_product__product_id=self.pk) - rating = feedbacks.aggregate(Avg("rating"))["rating__avg"] or 0 - cache.set(cache_key, rating, 86400) + feedbacks = Feedback.objects.filter(order_product__product_id=self.pk) + rating = feedbacks.aggregate(Avg("rating"))["rating__avg"] or 0 return float(round(rating, 2)) @rating.setter def rating(self, value: float): self.__dict__["rating"] = value - @property + @cached_property def feedbacks_count(self) -> int: - cache_key = f"product_feedbacks_count_{self.pk}" - feedbacks_count = cache.get(cache_key) - if feedbacks_count is None: - feedbacks_count = Feedback.objects.filter(order_product__product_id=self.pk).count() - cache.set(cache_key, feedbacks_count, 604800) - return feedbacks_count + return Feedback.objects.filter(order_product__product_id=self.pk).count() @property def price(self: Self) -> float: - stock = self.stocks.all().order_by("-price").only("price").first() - price = stock.price if stock else 0.0 - return round(price, 2) + stock = self.stocks.only("price").order_by("-price").first() + return round(stock.price, 2) if stock else 0.0 - @property + @cached_property def quantity(self) -> int: - cache_key = f"product_quantity_{self.pk}" - quantity = cache.get(cache_key, 0) - if not quantity: - stocks = self.stocks.only("quantity") - for stock in stocks: - quantity += stock.quantity - cache.set(cache_key, quantity, 3600) - return quantity + return self.stocks.aggregate(total=Sum("quantity"))["total"] or 0 @property def total_orders(self): @@ -1178,6 +1171,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi class Meta: verbose_name = _("order") verbose_name_plural = _("orders") + indexes = [ + Index(fields=["user", "status"]), + Index(fields=["status", "buy_time"]), + ] def __str__(self) -> str: return f"#{self.human_readable_id} for {self.user.email if self.user else 'unregistered user'}" @@ -1225,24 +1222,16 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi @property def total_price(self) -> float: - return ( - round( - sum( - ( - order_product.buy_price * order_product.quantity - if order_product.status not in FAILED_STATUSES and order_product.buy_price is not None - else 0.0 - ) - for order_product in self.order_products.all() - ), - 2, - ) - or 0.0 - ) + total = self.order_products.exclude(status__in=FAILED_STATUSES).aggregate( + total=Sum(F("buy_price") * F("quantity"), output_field=FloatField()) + )["total"] + + return round(total or 0.0, 2) @property def total_quantity(self) -> int: - return sum([op.quantity for op in self.order_products.all()]) + total = self.order_products.aggregate(total=Sum("quantity"))["total"] + return total or 0 def add_product( self, @@ -1676,6 +1665,8 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t verbose_name_plural = _("order products") indexes = [ GinIndex(fields=["notifications", "attributes"]), + Index(fields=["order", "status"]), + Index(fields=["product", "status"]), ] def return_balance_back(self): diff --git a/core/viewsets.py b/core/viewsets.py index b2051a6b..2a20d75e 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -5,7 +5,7 @@ from uuid import UUID from constance import config from django.conf import settings -from django.db.models import Prefetch, Q +from django.db.models import Prefetch, Q, OuterRef, Exists from django.http import Http404 from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator @@ -452,14 +452,23 @@ class ProductViewSet(EvibesViewSet): def get_queryset(self): qs = super().get_queryset() + + qs = qs.select_related("brand", "category") + if self.request.user.has_perm("core.view_product"): return qs - return qs.filter( - is_active=True, - brand__is_active=True, - category__is_active=True, - stocks__isnull=False, - stocks__vendor__is_active=True, + + active_stocks = Stock.objects.filter(product_id=OuterRef("pk"), vendor__is_active=True) + + return ( + qs.filter( + is_active=True, + brand__is_active=True, + category__is_active=True, + ) + .annotate(has_active_stocks=Exists(active_stocks)) + .filter(has_active_stocks=True) + .distinct() ) def get_object(self):