Django 注释返回重复条目

问题描述 投票:0回答:2

我正在像这样注释查询集:

class ItemQuerySet(models.QuerySet):
    def annotate_subitem_stats(self):
        return self.annotate(
            count_subitems=Count('subitems'),
            has_sent_subitems=Case(
                When(subitems__status=Status.sent, then=Value(True)),
                default=Value(False)
            ),
        )

在此示例中,

SubItem
是一个具有
Item
外键的模型。

当我运行此代码时,会发生奇怪的行为。假设我们有 1 个

Item
和 2 个
SubItem
与其链接。一个子项目的状态已发送,而另一子项目则没有。当我在查询集上运行注释时,查询集返回该项目两次,一次将
has_sent_subitems
设置为
True
,另一个设置为
False
。另一件奇怪的事情是,一个重复项有
count_subitems == 1
,另一个有
count_subitems == 1
,就好像查询集将项目分成两行,一行是
status == 'sent'
,另一行是
status != 'sent'

这基本上就是带注释的查询集的样子:

[
    {
        'name': 'Item Name',
        'count_subitems': 1,
        'has_sent_subitem': False
    },
    {
        'name': 'Item Name',
        'count_subitems': 1,
        'has_sent_subitem': True
    }
]

这就是数据库的样子,使用伪代码:

item = Item()
SubItem(item=item, status=draft)
SubItem(item=item, status=sent)

我很确定这与

When(subitems__status=Status.sent, then=Value(True)),
线有关。有什么方法可以让该行检查是否只有 1 个项目已发送状态,然后将注释设置为 true 并继续?

附注使用

.distinct()
不起作用。我不能使用
.distinct(field)
,因为
annotate() + distinct(fields) is not implemented.

django django-queryset django-annotate
2个回答
6
投票

您可以使用

Exists
子查询来避免
subitems__status
导致的连接,所以:

from django.db.models import Exists, OuterRef


class ItemQuerySet(models.QuerySet):
    def annotate_subitem_stats(self):
        return self.annotate(
            count_subitems=Count('subitems'),
            has_sent_subitems=Exists(
                SubItem.objects.filter(item=OuterRef('pk'), status=Status.sent)
            ),
        )

0
投票

对于一些额外的上下文:我认为这是一个已知且有记录的限制(尽管文档很容易被忽略)。 关于聚合的文档有这一部分(从 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

另一种规避限制的方法是将聚合放在子查询中。我之前解释过如何使用 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'),
)

还有一个 bug 报告跟踪此限制

© www.soinside.com 2019 - 2024. All rights reserved.