Django: несколько сгруппированных аннотаций с фильтрами ManyToMany - PullRequest
4 голосов
/ 28 мая 2020

Я столкнулся с проблемой, связанной с серией сгруппированных аннотаций в Django QuerySet, который фильтруется ManyToMany. Поскольку я слишком долго смотрел на свой экран, давайте представим серию моделей о дегустациях вин:

class Event(model.Model):
     # Some Fields

class Wine(models.Model):
     # Some Fields

class Tasting(models.Model):
    event = models.ManyToManyField(Event)
    wine = models.ForeignKey(Wine)
    score = models.IntegerField()

Я хочу получить некоторое агрегирование данных по вину, при желании фильтруя определенные события. С этим образцом данных дегустации (на которых я буду запускать агрегирование):

| wine_id | score | event_ids |
| ------- | ----- | --------- |
| 1       | 50    | [1]       |
| 1       | 50    | [1]       |
| 1       | 50    | [1, 2]    |
| 2       | 100   | [1, 2]    |
| 2       | 150   | [1, 2]    |
| 3       | 75    | [1]       |

Ожидаемый результат для приведенных выше данных:

[
    {'wine_id': 1, 'total_scores': 150, 'average_scores': 50},
    {'wine_id': 2, 'total_scores': 250, 'average_scores': 125},
    {'wine_id': 3, 'total_scores': 75, 'average_scores': 75},
]

Попытка 1

Просто какие-то обычные values и annotation

Tasting.objects.filter(
   event__in=Event.objects.filter(id__in=[1,2])
).distinct().values('wine_id').annotate(
   total_scores=Sum('score'),
   average_scores=Avg('scores'),
)

Какие выходят:

[
    {'wine_id': 1, 'total_scores': 200, 'average_scores': 50},  # Total score too high
    {'wine_id': 2, 'total_scores': 250, 'average_scores': 125},
    {'wine_id': 3, 'total_scores': 75, 'average_scores': 75},
]

Хм, так что, похоже, я сталкиваюсь с тем же самым проблема, которая возникает с множественными аннотациями - из-за объединения при фильтрации событий одна из wine_1 строк учитывается дважды: один раз для каждого события.

Попытка 2

Итак, глядя на кучу предложений из этой Django проблемы (например, этот ответ , я решил, что смогу решить проблему с помощью подзапросов, который привел меня к этому зверю:

total_subquery = Subquery(Tasting.objects.filter(wine_id=OuterRef('wine_id')).annotate(
   total_scores=Sum('score'),
).values('total_scores'))

average_subquery = Subquery(Tasting.objects.filter(wine_id=OuterRef('wine_id')).annotate(
   average_scores=Avg('scores'),
).values('average_scores'))

Tasting.objects.filter(
   event__in=Event.objects.filter(id__in=[1,2])
).distinct().values('wine_id').annotate(
   total_scores=total_subquery,
   average_scores=average_subquery,
)

Итак, изначально это выглядело правильно:

[
    {'wine_id': 1, 'total_scores': 150, 'average_scores': 50},
    {'wine_id': 2, 'total_scores': 250, 'average_scores': 125},
    {'wine_id': 3, 'total_scores': 75, 'average_scores': 75},
]

Ура! НО, что, если мы изменим фильтр, чтобы включить только событие 2:

Tasting.objects.filter(
   event__in=Event.objects.filter(id__in=[2])
).distinct().values('wine_id').annotate(
   total_scores=total_subquery,
   average_scores=average_subquery,
)

В этом случае я все равно получаю данные для ВСЕХ событий. Это имеет интуитивный смысл, поскольку подзапросы ничего не знают о внешнем фильтре. Однако, если я изменю OuterRef значений в подзапросе (до чего-то вроде filter(pk=OuterRef('pk'))), то правильная группировка в подзапросах разваливается. Если я повторно добавлю фильтрацию событий на уровне подзапроса, то мы получим ту же проблему с повторяющейся строкой, что и в нашем первом

Я могу получить правильные значения, просто получив все данные и затем выполнив агрегирование в Python, но это серьезно снижает производительность для больших наборов данных. Есть ли способ сделать это агрегирование ion полностью через ORM?

1 Ответ

3 голосов
/ 04 июня 2020

надеюсь, вы не слишком разочарованы:)

Что вам нужно сделать в этом случае, так это избежать объединения таблицы Events, чтобы остановить безумие двойного счета.

Wine.objects.filter(
    tasting__in=Tasting.event.through.objects.filter(event_id__in=[1, 2]).values('tasting_id')
).values('id').annotate(
    total_score=Sum('tasting__score'),
    average_scores=Avg('tasting__score')
)
[
{'id': 1, 'total_score': 150, 'average_scores': 50.0}, 
{'id': 2, 'total_score': 250, 'average_scores': 125.0}, 
{'id': 3, 'total_score': 75, 'average_scores': 75.0}
]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...