【问题标题】:Django - Annotating an aggregation of annotations on related objectsDjango - 对相关对象的注释聚合进行注释
【发布时间】:2021-11-15 17:16:24
【问题描述】:

我有三个模型:

class BaseModel(Model):
    deleted = BooleanField(default=False)


class Document(BaseModel):
    def total_price()
        return DocumentLine.objects.filter(
            section__in=self.sections.filter(deleted=False),
            deleted=False,
        ).total_price()


class Section(BaseModel):
    document = ForeignKey(Document, on_delete=CASCADE, related_name='sections')


class LineQuerySet(QuerySet):
    def with_total_price(self):
        total_price = F('quantity') * F('price')
        return self.annotate(
            total_price=ExpressionWrapper(total_price, output_field=DecimalField())
        )
    
    def total_price(self):
        return self.with_total_prices().aggregate(
            Sum('total_price', output_field=DecimalField())
        )['total_price__sum'] or Decimal(0.0)


class Line(BaseModel):
    objects = LineQuerySet.as_manager()
    section = ForeignKey(Section, on_delete=CASCADE, related_name='lines')

    price = DecimalField()
    quantity = DecimalField()

正如您在LineQuerySet 上看到的那样,有一种方法可以根据价格和数量用每行的总价格注释查询集。

现在我可以通过这样的方式轻松获得整个文档的总价格(注意带有deleted=True 的行和部分会被忽略):

document = Document.objects.get(pk=1)
total_price = document.total_price()

但是,现在我想生成一个包含多个文档的查询集,并使用每个文档的 total price 对其进行注释。我尝试了注释、聚合、使用 prefetch_related(使用 Prefetch)和 OuterRef 的组合,但我似乎无法在不引发错误的情况下获得我想要的结果。

是否有某种方法可以在查询集中执行此操作,从而可以通过此 total_price 字段进行过滤或排序?

【问题讨论】:

    标签: python django


    【解决方案1】:

    您可以使用以下方式进行注释:

    from django.db.models import F, Sum
    
    Document.objects.filter(
        deleted=False,
        sections__deleted=False,
        section__lines__deleted=False
    ).annotate(
        total_price=Sum(F('sections__lines__price')*F('sections__lines__quantity'))
    )

    从这个查询集中产生的每个Document 都会有一个属性.total_price,它是价格的总和乘以所有相关行的所有相关行的数量Document的部分。

    另一种方法是使用Subquery expression [Django-doc] 来确定总和,因此:

    from django.db.models import F, OuterRef, Subquery, Sum
    
    Document.objects.annotate(
        total_price=Subquery(
            Line.objects.values(
                document_id=F('section__document_id')
            ).filter(
                deleted=False, section__deleted=False, document__deleted=False
            ).annotate(
                total_price=Sum(F('price') * F('quantity'))
            ).order_by('document_id').filter(document_id=OuterRef('pk')).values('total_price')[:1]
        )
    )

    【讨论】:

    • 这让我更接近我想要的结果,但看起来我遗漏了一些关键信息。我已经更新了帖子。每个模型还有一个deleted 标志,当True 意味着它应该从total_price 中忽略。如果一个线组被删除,所有相关的线也应该被忽略,无论它们是否被删除。
    • 几乎就在那里。现在唯一的问题是一个被删除的单个部分的文档将不会包含在查询集中,因为该文档不满足过滤器。与具有部分但该部分中没有行的文档相同。
    • 我终于用你上次的编辑让它工作了。我确实需要添加另一个.values('total_price') 来过滤掉document_id,以防止返回多个列的错误。我不太明白的一件事是 Line 查询集上的 Sum 注释。这是否会将总和的结果添加到 Line 结果查询中的每一行?之后你只选择第一个?
    • @Swan:我们在子查询中的document_id 上创建一个GROUP BY,因此它将通过.annotate(..) 确定该组的总和。
    猜你喜欢
    • 2022-12-16
    • 2016-11-18
    • 2011-04-06
    • 2015-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-16
    • 1970-01-01
    相关资源
    最近更新 更多