Дублирование строк при агрегировании над values() с отношением «многие-ко-многим
Я пытаюсь вычислить поиск для элементов, имеющих общий связанный объект + общее значение. Кажется, что это должно быть довольно просто, но я продолжаю получать неправильные результаты, что бы я ни пробовал. Я не могу добиться того, чтобы группировка была такой, как я хочу.
Вот простой пример: У меня есть Persons
, назначенный на Teams
; у меня есть ExpenseReports
, подписанный (несколькими) Persons
на определенные даты.
Мне нужен запрос, который для каждой пары Team
и date
находит общую сумму расходов, подписанную этой командой на эту дату.
Вот мои модели:
class MyTeam(models.Model):
name = models.CharField()
class MyPerson(models.Model):
name = models.CharField()
team = models.ForeignKey(MyTeam, on_delete=models.CASCADE)
class ExpenseReport(models.Model):
expense_paid = models.FloatField()
expense_date = models.DateField()
persons = models.ManyToManyField(MyPerson)
А вот простые данные - отчеты о расходах за две даты.
Appa
и Alex
на Team A
; Barbara
и Bob
на Team B
:
[2024-11-01] 1.0 paid by [<MyPerson: Person <Alex>>, <MyPerson: Person <Appa>>] <-- Team A
[2024-11-01] 10.0 paid by [<MyPerson: Person <Barbara>>, <MyPerson: Person <Bob>>] <-- Team B
[2024-11-05] 100.0 paid by [<MyPerson: Person <Barbara>>] <-- Team B
[2024-11-05] 1000.0 paid by [<MyPerson: Person <Alex>>, <MyPerson: Person <Bob>>] <-- Teams A and B
Что я пробовал
Есть очевидная наивная реализация - которая неверна, поскольку не учитывает дубликаты строк:
reports_qs = ExpenseReport.objects.all()
rows = reports_qs.values("expense_date", "persons__team__name").annotate(total_expense=Sum("expense_paid"))
Легко заметить, что эти результаты неверны - значения с двумя членами одной команды удваиваются:
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'B Team', 'total_expense': 20.0} <-- doubled
{'expense_date': datetime.date(2024, 11, 5), 'persons__team__name': 'B Team', 'total_expense': 1100.0}
{'expense_date': datetime.date(2024, 11, 5), 'persons__team__name': 'A Team', 'total_expense': 1000.0}
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'A Team', 'total_expense': 2.0} <-- doubled
Но я думал, что решение заключается в использовании подзапроса, и это тоже не сработало:
reports_qs = ExpenseReport.objects.all()
subquery = (
ExpenseReport.objects.filter(
expense_date=OuterRef("expense_date"),
persons__team__name=OuterRef("persons__team__name"),
)
.values("expense_date", "persons__team__name")
.annotate(total_expense=Sum("expense_paid"))
.values("total_expense")
)
rows = reports_qs.values("expense_date", "persons__team__name").annotate(total_expense=subquery[:1])
Это дало результат:
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'A Team', 'total_expense': 2.0} <-- doubled
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'A Team', 'total_expense': 2.0} <-- doubled
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'B Team', 'total_expense': 20.0} <-- doubled
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'B Team', 'total_expense': 20.0} <-- doubled
{'expense_date': datetime.date(2024, 11, 5), 'persons__team__name': 'B Team', 'total_expense': 1100.0}
{'expense_date': datetime.date(2024, 11, 5), 'persons__team__name': 'B Team', 'total_expense': 1100.0}
{'expense_date': datetime.date(2024, 11, 5), 'persons__team__name': 'A Team', 'total_expense': 1000.0}
- и другие отчаянные попытки засунуть distinct()
куда-нибудь полезное не решили вопроса.
Я также вижу, что я прямо получаю другой результат от структуры values().annotate()
, чем при вызове aggregate()
:
expense_date, team_name = example_report.expense_date, example_report.persons.first().team.name
# values().annotate()
res1 = (
ExpenseReport.objects.filter(
expense_date=expense_date,
persons__team__name=team_name,
)
.values("expense_date", "persons__team__name")
.annotate(total_expense=Sum("expense_paid"))
.values("total_expense")
)
# aggregate()
res2 = (
ExpenseReport.objects.filter(
expense_date=expense_date,
persons__team__name=team_name,
)
.distinct()
.aggregate(total_expense=Sum("expense_paid"))
)
В результате:
> res1=<QuerySet [{'total_expense': 2.0}]> # ...doubled yet again
> res2={'total_expense': 1.0} # correct
Возможно ли выполнить агрегацию, которую я пытаюсь сделать, в рамках Django ORM? Что я делаю не так?
Если гарантируется, что все .person
из ExpenseReport
всегда будут одинаковыми Team
, мы можем сделать это с помощью выражения Subquery
[Django-doc]:
from django.db.models import OuterRef, Subquery, Sum
ExpenseReport.objects.values(
'expense_date',
team_id=Subquery(
MyPerson.objects.filter(
expensereport=OuterRef('pk')
).values('team_id')[:1]
),
).annotate(total_expense=Sum('expense_paid')).order_by('expense_date', 'team_id')
Таким образом, Subquery(…)
получает первичный ключ команды « первого» человека (в любом порядке). Мы предполагаем, что все Person
имеют одну и ту же команду.
Вопрос скорее в том, что делать, если расходы распределяются на несколько команд.