我正在尝试计算共享公共相关对象+公共值的项目的查找。作为非正式的总结,我想使用某种形式:
my_queryset.values(my_related_obj, my_date).annotate(...)
--为了获得每个
(related_obj, date)
对的查询集中匹配项的 Sum 聚合。
本例中的
related_obj
是两个连接——一个多对多关系,然后是一个外键。因此,肯定存在具有相关对象的多个值的查询集项,或者通过不同的中间对象到达相同的相关对象。这看起来应该是可行的,但无论我尝试什么,我都会得到不正确的结果。我无法让分组达到我想要的效果。
这是一个简单的例子:我将
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
有了这些数据,我正在寻找的结果是:
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'A Team', 'total_expense': 1.0}
{'expense_date': datetime.date(2024, 11, 1), 'persons__team__name': 'B Team', 'total_expense': 10.0}
{'expense_date': datetime.date(2024, 11, 5), 'persons__team__name': 'A Team', 'total_expense': 1000.0}
{'expense_date': datetime.date(2024, 11, 5), 'persons__team__name': 'B Team', 'total_expense': 1100.0}
我尝试过的事情
有一个明显的幼稚实现——这是不正确的,因为它没有考虑重复的行:
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')
问题更多的是,如果费用分摊给多个团队该怎么办。