我有以下型号:
class Institution(models.Model):
pass
class Headquarter(models.Model):
institution = models.ForeignKey(Institution, related_name='headquarters')
class Audit(models.Model):
headquarter = models.ForeignKey(Headquarter, related_name='audits')
以及以下查询(基本上,如果一个机构至少进行过一次审核,则 has_visits 必须为 true:
Institution.objects.annotate(
has_visits=models.Case(
models.When(headquarters__audits=None, then=False),
default=True,
output_field=models.BooleanField()
)
)
问题是,如果一个机构有 2 次审计,那么查询集会返回重复的行。我想这与 SQL 级别的连接有关,但我不知道如何纠正它。我找到了这个answer,但我不认为 OuterRef 是我在我的情况下正在寻找的。实现这一目标的正确方法是什么?
Exists
子查询 [Django-doc]:
from django.db.models import Exists, OuterRef
Institution.objects.annotate(
has_visits=Exists(
Audit.objects.filter(headquarter__institution=OuterRef('pk'))
)
)
其他可能有帮助的方法是让重复项“折叠”,例如使用
Max
:
from django.db.models import Max, Value
from django.db.models.functions import Coalesce
Institution.objects.annotate(
has_visits=Coalesce(Max(models.Case(
models.When(headquarters__audits=None, then=False),
default=True,
output_field=models.BooleanField()
)), Value(False))
)
但这可能会降低其可读性和效率。
对于一些额外的上下文:我认为这与已知的和记录的限制有关(尽管文档很容易被忽略)。 关于聚合的文档有这一部分(从 Django 4.2 文档复制):
组合多个聚合
使用 annotate() 组合多个聚合将产生错误的结果,因为使用连接而不是子查询:
>>> book = Book.objects.first() >>> book.authors.count() 2 >>> book.store_set.count() 3 >>> q = Book.objects.annotate(Count('authors'), Count('store')) >>> q[0].authors__count 6 >>> q[0].store__count 6
对于大多数聚合,无法避免此问题,但是,Count 聚合有一个可能有帮助的独特参数:
>>> q = Book.objects.annotate(Count('authors', distinct=True), Count('store', distinct=True)) >>> q[0].authors__count 2 >>> q[0].store__count 3
本文档是关于在注释中生成连接的聚合,但我怀疑自己生成连接的注释本质上也会发生相同的情况,就像OP的示例一样。
另一种规避限制的方法(尽管它可能不适用于OP的问题)是将聚合放在子查询中。我之前解释过如何使用 django-sql-utils 包来实现此目的:
对于遇到这种情况的其他人来说,一个合理的解决方法似乎是使用子查询来聚合注释。目前,这在 Django 中有点冗长/hacky,正如来自Antoine's comment的 stackoverflow 链接中的多种方法所示。不过,我刚才已经成功使用了 django-sql-utils 包。这听起来有点庞大,但它只有两个实用程序,其中之一是 SubqueryAggregate 类(具有派生的 SubqueryCount、SubquerySum 等),它使将常规连接聚合转换为子查询聚合变得简单而简洁,而无需过多更改结构.
例如,采用 comment 66 中的示例,并使用 django-sql-utils 将第二个注释转换为子查询,您将得到:
Branch.objects.annotate( total=Sum('center__client__loan__amount'), repaid=SubquerySum('center__client__loan__payment_schedule__payments__principal'), )