【问题标题】:Django: using an annotated aggregate in queryset update()Django:在查询集更新()中使用带注释的聚合
【发布时间】:2018-06-15 14:05:58
【问题描述】:

我在添加到现有项目的新应用程序中遇到了一个有趣的情况。我的目标是(使用 Celery 任务)使用包含来自外键对象的注释聚合值的值一次更新多行。以下是我在之前的问题中使用的一些示例模型:

class Book(models.model):
    author = models.CharField()
    num_pages = models.IntegerField()
    num_chapters = models.IntegerField()

class UserBookRead(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    user_book_stats = models.ForeignKey(UserBookStats)
    book = models.ForeignKey(Book)
    complete = models.BooleanField(default=False)
    pages_read = models.IntegerField()

class UserBookStats(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    total_pages_read = models.IntegerField()

我正在尝试:

  1. Book 页计数更新时,使用来自Book 实例的post_save 信号更新相关UserBookRead 对象上的pages_read
  2. 在信号结束时,启动后台Celery任务,从每个更新的UserBookRead中汇总pages_read,并更新每个相关UserBookStats上的total_pages_read(这就是问题所在)

我正在尝试尽可能精简查询数量 - 步骤 1 已完成,并且只需要针对我的实际用例进行一些查询,这对于信号处理程序来说似乎是可以接受的,只要这些查询是适当优化。

第 2 步涉及更多,因此委托给后台任务。我已经设法以相当干净的方式完成了大部分工作(至少对我而言)。

我遇到的问题是,当使用total_pages 聚合注释UserBookStats 查询集(所有pages_read 中的Sum() 用于相关的UserBookRead 对象)时,我无法直接使用查询集的update 设置total_pages_read 字段。

这是代码(Book 实例以book 的形式传递给任务):

# use the provided book instance to get the stats which need to be updated
book_read_objects= UserBookRead.objects.filter(book=book)
book_stat_objects = UserBookStats.objects.filter(id__in=book_read_objects.values_list('user_book_stats__id', flat=True).distinct())

# annotate top level stats objects with summed page count
book_stat_objects = book_stat_objects.annotate(total_pages=Sum(F('user_book_read__pages_read')))

# update the objects with that sum
book_stat_objects.update(total_pages_read=F('total_pages'))

在执行最后一行时,抛出此错误:

django.core.exceptions.FieldError: Aggregate functions are not allowed in this query

经过一番研究,我发现了此用例 here 的现有 Django 票证,其中最后一条评论提到了 1.11 中的 2 个新功能,这可能使之成为可能。

是否有任何已知/可接受的方法来完成此用例,可能使用SubqueryOuterRef?我没有成功尝试将聚合折叠为Subquery。这里的后备是:

for obj in book_stat_objects:
    obj.total_pages_read = obj.total_pages
    obj.save()

但是book_stat_objects 中可能有数万条记录,我真的在努力避免单独为每条记录发出更新。

【问题讨论】:

    标签: django django-orm


    【解决方案1】:

    我最终弄清楚了如何使用 SubqueryOuterRef 做到这一点,但不得不采取与我最初预期不同的方法。

    我能够快速得到一个 Subquery 工作,但是当我用它来注释父查询时,我注意到每个注释值都是子查询的 first 结果 - 这是当我意识到我需要OuterRef,因为生成的 SQL 并没有通过父查询中的任何内容来限制子查询。

    This Django 文档的一部分非常有帮助,this StackOverflow 问题也是如此。此过程归结为您必须使用Subquery 创建聚合,并使用OuterRef 确保子查询通过父查询PK 限制聚合行。此时,您可以使用聚合值进行注释并直接在查询集update() 中使用它。

    正如我在问题中提到的,代码示例是编造的。我试图通过我的更改使它们适应我的实际用例:

    from django.db.models import Subquery, OuterRef
    from django.db.models.functions import Coalesce
    
    # create the queryset to use as the subquery, restrict based on the `book_stat_objects` queryset
    book_reads = UserBookRead.objects.filter(user_book_stat__in=book_stat_objects, user_book_stats=OuterRef('pk')).values('user_book_stats')
    # annotate the future subquery with the aggregation of pages_read from each UserBookRead
    total_pages = book_reads.annotate(total=Sum(F('pages_read')))
    # annotate each stat object with the subquery total
    book_stats = book_stats.annotate(total=Coalesce(Subquery(total_pages), 0))
    # update each row with the new total pages count
    book_stats.update(total_pages_read=F('total'))
    

    创建一个不能单独使用的查询集感觉很奇怪(尝试评估 book_reads 会由于包含 OuterRef 而引发错误),但是一旦您检查了为 book_stats 生成的最终 SQL ,有道理。

    编辑

    在弄清楚这个答案后一两个星期,我最终遇到了这个代码的错误。原来是由于UserBookRead 模型的默认ordering。作为Django docs 状态,默认ordering 被合并到任何聚合GROUP BY 子句中,所以我所有的聚合都关闭了。解决方案是在创建基本子查询时使用空白order_by() 清除默认排序:

    book_reads = UserBookRead.objects.filter(user_book_stat__in=book_stat_objects, user_book_stats=OuterRef('pk')).values('user_book_stats').order_by()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-08
      • 2019-08-03
      • 2020-05-23
      • 2023-03-29
      • 1970-01-01
      • 2017-07-21
      • 2020-05-14
      • 2016-08-17
      相关资源
      最近更新 更多