代码之家  ›  专栏  ›  技术社区  ›  dkhaupt

Django:在queryset update()中使用带注释的聚合

  •  4
  • dkhaupt  · 技术社区  · 7 年前

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

    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. 使用 post_save 信号来自 Book 要更新的实例 pages_read 关于相关 UserBookRead 对象时 页面计数已更新。
    2. 在信号结束时,启动后台芹菜任务,将 pages\u已读 从每个 用户BookRead 已更新,并更新 total_pages_read 在每个相关的 UserBookStats (这就是问题发生的地方)

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

    第2步更为复杂,因此授权给后台任务。我已经设法以一种相当干净的方式完成了大部分工作(至少对我来说是这样)。

    我遇到的问题是,在注释 用户图书状态 查询设置为 total_pages 聚合(即 Sum() 其中 pages\u已读 对于相关 用户BookRead 对象),我不能用直线 update 要设置 total\u pages\u read总页数 领域

    这是代码 实例作为传递给任务 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
    

    经过一些研究,我找到了这个用例的现有Django票证 here ,最后一条评论提到了1.11中的两个新特性,这可能使其成为可能。

    是否有任何已知/公认的方法来完成此用例,可能是使用 Subquery OuterRef ? 我没有成功地将聚合作为 子查询 . 这里的退路是:

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

    但可能有数以万计的记录 book_stat_objects ,我真的在努力避免为每一个单独发布更新。

    1 回复  |  直到 7 年前
        1
  •  12
  •   dkhaupt    7 年前

    我终于想出了如何用 Subquery OuterRef ,但不得不采取与我最初预期不同的方法。

    我很快就得到了 子查询 然而,在工作时,当我使用它来注释父查询时,我注意到每个注释的值都是 第一 子查询的结果-这是我意识到我需要 OuterRef公司 ,因为生成的SQL没有通过父查询中的任何内容限制子查询。

    This Django文档的一部分非常有用 this 堆栈溢出问题。这个过程归结起来就是 子查询 创建聚合,以及 OuterRef公司 确保子查询通过父查询主键限制聚合行。此时,您可以使用聚合值进行注释,并在查询集中直接使用它 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公司 ),但一旦检查为生成的最终SQL book_stats ,这是有道理的。

    编辑

    在找到这个答案一两周后,我最终遇到了一个错误。结果是由于违约 ordering 对于 UserBookRead 模型作为 Django docs 状态,默认值 订购 并入任何骨料中 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()