Дублирование строк при агрегировании над 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 имеют одну и ту же команду.

Вопрос скорее в том, что делать, если расходы распределяются на несколько команд.

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