Аннотация Django ORM для поиска того, сколько объектов имеют какой-либо связанный объект
В настоящее время я пытаюсь выяснить, какой процент из набора объектов имеет связанный объект с определенными значениями. В частности, у меня есть таблица объектов и связь один ко многим с таблицей комментариев, и я пытаюсь выяснить, какой процент этих объектов имеет комментарии определенной длины. Обе эти таблицы являются ETL-выводом из отдельного набора данных, чтобы облегчить расчеты метрик.
# Models
class Data(models.Model):
id = models.AutoField(primary_key=True)
creator_id = models.IntegerField() # Not a real foreign key
class DataCommenter(models.Model):
do_id = models.ForeignKey(Data)
creator_id = models.IntegerField() # Not a real foreign key
short_comments = models.IntegerField()
medium_comments = models.IntegerField()
long_comments = models.IntegerField()
Из этих моделей у меня есть несколько аннотаций запросов, которые выполняются для получения среднего значения, как показано ниже:
# QuerySet
class DataQuerySet(models.QuerySet):
def extensive_comments(self):
"""Get extensive comment raw selection."""
inner_query = DataCommenter.objects.exclude(
creator_id=OuterRef("creator_id")
).filter(
Q(medium_comments__gte=1) | Q(long_comments__gte=1), do_id=OuterRef("id")
)
return self.annotate(
raw_extensive_comments=Case(
When(
Exists(inner_query), then=1
), default=0, output_field=FloatField()
)
)
def annotate_period(self):
"""Annotation to allow average without aggregation."""
return self.annotate(period=Value(value=True, output_field=BooleanField()))
Набор QuerySet прикреплен к модели данных и используется следующим образом:
Data.objects.all().annotate_period().extensive_comments().values("period").annotate(
extensive_comments=ExpressionWrapper(Avg(raw_extensive_comments) * 100, output_field=FloatField())
)
Имеющиеся у нас данные включают несколько объектов DataCommenter для некоторых объектов Data, и по какой-то причине среднее значение рассчитывается по количеству объектов DataCommenter вместо количества объектов Data, поэтому то, что должно быть 3/5 объектов данных, дающих 60, мы получаем что-то вроде 10/12 и получаем 83.33333. У нас есть большое количество других метрик, которые мы вычисляем, используя ряд других полей, не показанных здесь, поэтому мы не можем использовать aggregate
, а использование values("period")
должно заставить объекты рассматриваться как одна группа для последующих аннотаций, которые включают Avg
, а они рассматриваются для каждой метрики, кроме этого единственного вычисления. Мы пробовали использовать внутренний_запрос непосредственно внутри аннотации, пробовали, чтобы этот внутренний запрос имел .values("do_id").distinct()
в конце, пробовали полностью удалить запрос и работать с использованием фильтрации типа commenter__medium_comments
непосредственно в модели данных, и я понятия не имею, почему это возвращается таким образом. Любая помощь будет очень признательна.
Аннотации, перечисленные в вопросе, полностью верны, а фактическая проблема заключалась в FilterSet в самом представлении. В частности, у нас был набор фильтров, выглядящий creator_id
следующим образом (связанное название модели DataCommenter для Data - commenter
):
class BaseAggregateFilterSet(filters.FilterSet):
creator_id = filters.NumberFilter(method=filter_by_creator_id")
def filter_by_creator_id(self, qs, _, value):
return qs.filter(Q(creator_id=value) | Q(commenter__creator_id=value)).distinct()
Я думал, что distinct()
в конце запроса гарантирует, что если у вас есть 1 объект Data с объектом DataCommenter с тем же creator_id, или даже с другим, который соответствует отбору, вы получите обратно только единичные строки для объектов Data. На самом деле происходит неявное соединение INNER JOIN, которое умножает каждый из ваших объектов Data на количество объектов DataCommenter. Правильный способ сделать это, чтобы гарантировать, что вы сохраните ваш QuerySet только из объектов Data, состоит в следующем:
def filter_by_creator_id(self, qs, _, value):
data_ids = qs.filter(Q(creator_id=value) | Q(commenter__creator_id=value)).values("id").distinct()
return qs.filter(id__in=data_ids)
После внесения этого изменения все наши аннотации, которые возвращали странные результаты для средних значений, были исправлены, поскольку теперь они оперировали ожидаемым количеством объектов вместо того, чтобы некоторые объекты считались несколько раз.