Эффективный способ упорядочить поле Annotate в DRF

У меня есть три модели в models.py,

class AAA(models.Model):
    name = models.CharField(max_length=300, null=False, blank=False)

class BBB(models.Model):
    aaa = models.ForeignKey(AAA, on_delete=models.CASCADE, null=False, blank=False)
    content = models.CharField(max_length=300, null=False, blank=False)

class CCC(models.Model):
    aaa = models.ForeignKey(AAA, on_delete=models.CASCADE, null=False, blank=False)
    name = models.CharField(max_length=300, null=False, blank=False)

и я хочу заказать в views.py,

class AAAListCreateView(generics.ListAPIView):
    class AAAPagination(CursorPagination):
        page_size = 2

    queryset = AAA.objects.all().annotate(
        b_count=Count("bbb", distinct=True),
        c_count=Count("ccc", distinct=True),
        total=F('b_count')+('c_count')
    )
    serializer_class = AAAListSerializer
    pagination_class = AAAPagination
    filter_backends = [DjangoFilterBackend, OrderingFilter]
    filterset_class = AAAListFilterSet
    ordering_fields = ['b_count', 'c_count', 'total']
    ordering = 'b_count'

Проблемы, которые я хочу задать...

  1. Я хочу упорядочить по убыванию все мои поля упорядочивания (ordering_fields). Нисходящий порядок без указания -.
  2. В views.py я аннотирую все поля упорядочивания. Но я думаю, что это неэффективный способ, потому что получает все сортировочные аннотированные поля.

Что мне делать?

Так как вы используете django-filter для упорядочивания, возможно, стоит переопределить класс OrderingFilter... в основном рассматривая метод filter()

Вот что я придумал. Нужно еще поработать, чтобы сделать его более динамичным, если для вас это обычная проблема; но для 1-2 случаев, в теории, это должно работать:

Фильтр по заказу

  • {project_root}/utils/django_filters_overrides.py
  • Примечание: Я прогнал это через ruff, но не тестировал
  • .
  • TODO: Фактические аннотации в def filter
from django.db.models import Count, F
from django.forms.utils import pretty_name
from django.utils.translation import gettext_lazy as _

from django_filters import OrderingFilter
from django_filters.constants import EMPTY_VALUES

class FancyOrderingFilter(OrderingFilter):
    """OrderingFilter Override, always descending order + dynamic annotate"""

    def get_ordering_value(self, param):
        """Override order, always descend"""

        # Keep all this so actually putting "-" still works
        descending = param.startswith("-")
        param = param[1:] if descending else param
        field_name = self.param_map.get(param, param)

        # return "-%s" % field_name if descending else field_name
        return "-%s" % field_name # always descend

    def filter(self, qs, value):
        """Override filter, dynamically annotate"""
        if value in EMPTY_VALUES:
            return qs

        ordering = [
            self.get_ordering_value(param)  # <- call override
            for param in value
            if param not in EMPTY_VALUES
        ]

        # manually annotate (sucks, but you've only got to do it once!)
        annotate_dict = {}
        if "-total" in ordering:
            annotate_dict["b_count"] = Count("bbb", distinct=True)
            annotate_dict["c_count"] = Count("ccc", distinct=True)
            annotate_dict["total"] = F("b_count")+("c_count")
        if "-b_count" in ordering and "-b_count" not in annotate_dict:
            annotate_dict["b_count"] = Count("bbb", distinct=True)
        if "-c_count" in ordering and "-c_count" not in annotate_dict:
            annotate_dict["c_count"] = Count("ccc", distinct=True)

        # NOTE: `**` on a dict turns them into keyword args
        #   I personally like this trick as it's only a single `.filter()` or
        #   `.annotate()` call
        qs = qs.annotate(**annotate_dict)

        # Normal Django ordering
        return qs.order_by(*ordering)

    def build_choices(self, fields, labels):
        """
        Override build choices, so labels are correct.
        See source, this no_dash label.get() is iffy.
        Not sure if this method is important :shrugs:
        """
        no_dash = [
            (param, labels.get("%s" % param, self.descending_fmt % labels.get(
                field, _(pretty_name(param))
            )))
            for field, param in fields.items()
        ]
        with_dash = [
            ("-%s" % param, labels.get("-%s" % param, self.descending_fmt % label))
            for param, label in no_dash
        ]

        # interleave the no dash and with dash choices
        return [val for pair in zip(no_dash, with_dash) for val in pair]

Новый набор видов

from utils.django_filters_overrides import FancyOrderingFilter

class AAAListCreateView(generics.ListAPIView):
    class AAAPagination(CursorPagination):
        page_size = 2

    queryset = AAA.objects.all()  # <- change
    serializer_class = AAAListSerializer
    pagination_class = AAAPagination
    filter_backends = [DjangoFilterBackend, FancyOrderingFilter]  # <- change
    filterset_class = AAAListFilterSet
    ordering_fields = ['b_count', 'c_count', 'total']
    ordering = 'b_count'

.all() аннотируйте вопрос

Теперь filter_backends = [DjangoFilterBackend, FancyOrderingFilter] заставляет меня думать, что он буквально запускает base_qs -> DjangoFilterBackend -> FancyOrderingFilter. Так что если бы вы могли сделать print(qs.count()) внутри этого FancyOrderingFilter.filter метода и проверить его на AAA.objects.all().count(), было бы здорово. Надеюсь, что да! Потому что это означает, что мы закончили, за вычетом обычной отладки.

Вернуться на верх