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

时间:2018-01-05 18:08:57

标签: django django-orm

我在一个新的应用程序中遇到了一个有趣的情况,我添加到现有项目中。我的目标是(使用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. 在更新post_save页数时,使用Book个实例中的pages_read信号更新相关UserBookRead个对象的Book
  2. 在信号结束时,启动后台Celery任务以汇总已更新的每个pages_read中的UserBookRead,并更新每个相关total_pages_read上的UserBookStats (这是问题发生的地方)
  3. 对于查询数量而言,我尽可能地精简 - 第1步已完成,只需要对我的实际用例进行一些查询,这对于信号处理程序来说似乎是可以接受的,只要这些查询是正确优化。

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

    我遇到的问题是,使用UserBookStats聚合注释total_pages查询集时(相关Sum()个对象的所有pages_read的{​​{1}}) ,我无法通过查询集的直接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'))
    

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

    是否有任何已知/可接受的方法来完成此用例,可能使用django.core.exceptions.FieldError: Aggregate functions are not allowed in this query Subquery?我没有成功尝试将聚合折叠为OuterRef。这里的后备是:

    Subquery

    但是for obj in book_stat_objects: obj.total_pages_read = obj.total_pages obj.save() 中可能存在数万条记录,我真的试图避免为每一条记录单独发布更新。

1 个答案:

答案 0 :(得分:5)

我最终弄清楚如何使用SubqueryOuterRef执行此操作,但必须采取与我原先预期不同的方法。

我能够快速获得Subquery工作,但是当我用它来注释父查询时,我注意到每个带注释的值都是子查询的第一个结果 - 这个当我意识到我需要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,这是有道理的。

修改

在弄清楚这个答案之后,我在一两个星期内遇到了这个代码的错误。结果是由于ordering模型的默认UserBookRead。作为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()