【问题标题】:Annotate Total Count of Descents of Mptt Model标注 Mptt 模型的下降总数
【发布时间】:2020-08-30 09:34:31
【问题描述】:

问题

鉴于以下模型,我想获取所有页面的查询集,并用与页面关联的线程中的 cmets 总数进行注释,包括与页面关联的评论线程树中的所有 cmets。

我正在使用django-mptt 来存储评论树。

我可以使用comment.get_descendant_count()在python中得到这个,但是在查询所有页面时这是非常低效的

型号

class CommentThread(models.Model):
    ...


class Page(models.Model):
    ...
    thread = models.ForeignKey("CommentThread", ...)   



class Comment(MPTTModel):
    body = models.TextField()
    author = models.ForeignKey("User", ...)

    # Validation ensures a comment has
    # a thread or parent comment but not both
    thread = models.ForeignKey("CommentThread", related_name="comments", ...)
    parent = TreeForeignKey(
                'self',
                on_delete=models.CASCADE,
                null=True,
                blank=True,
                related_name='children'
    )

    class MPTTMeta:
        level_attr = 'mptt_level'
        order_insertion_by=['name']

此模型允许我向页面添加多个“根 cmets”,但也可以递归地将 cmets 嵌套在每个评论下作为回复。


# Model Use Examples

thread = CommentThread()
page = Page(thread=thread)

# add page level root comments
comment1 = Comment(thread=thread, ...)
comment2 = Comment(thread=thread, ...)
comment3 = Comment(thread=thread, ...)

# add Comment Replies to comment #1
comment_reply1 = Comment(parent=comment1, ...)
comment_reply2 = Comment(parent=comment1, ...)
comment_reply3 = Comment(parent=comment1, ...)

当前方法 - 在 python 中

有效但非常低效:

page = Page.objects.first()
total_comments = [c.get_descendant_count() for c in page.thread.comments.all()]

我尝试过的

我不确定如何使用查询集和注释来实现这一点。 我知道每个 mptt 模型也会得到一个treed_id,所以我猜我需要构建一个更复杂的子查询。

要仅获取根 cmets 的数量(不包括嵌套),我可以这样做:

pages = Page.objects.all().annotate(num_comments=models.Count("thread__comments"))
num_root_comments = pages[0].num_comments

再一次,目标是获取所有 cmets,包括嵌套:

# Non working code - this kind of  pseudo queryset code of what I am trying:

all_page_comments = Comment.objects.filter(tree_id__in= (Page.thread__comments__tree_id))
Page.objects.all().annotate(num_comments=Count(Subquery(all_page_comments))

提前感谢您提供的任何帮助。

解决方案

感谢@andrey 在下面的回答,得到了一个可行的解决方案。 不确定它是否最佳,但似乎在单个查询中返回正确的值。

threads = CommentThread.objects.filter(
        id=models.OuterRef("thread")
    ).annotate(
        comment_count=models.Sum(
            Floor((models.F("comments__rght") - models.F("comments__lft") - 1) / 2)
        )
    )

qs_pages_with_comment_count = (
    Page.objects
    .annotate(
        comment_count=models.Subquery(
            threads.values("comment_count")[:1], output_field=models.IntegerField()
        )
    )
    # Count from subquery included count of descendents of 
    # each "root" comment but not the root comment itself
    # so we add  number of root comments per thread on top
    .annotate(
        comment_count=models.F("comment_count")
        + models.Count("thread__comments", distinct=True)
    )
)

【问题讨论】:

    标签: django django-models orm django-mptt


    【解决方案1】:
    queryset.annotate(
        descendants_count=Floor((F('rght') - F('lft') - 1) / 2)
    ).values(
        'descendants_count'
    ).aggregate(
        total_count=Count('descendants_count')
    )
    

    让我解释一下

    首先,get_descendant_count 的当前方法只是操作现有数据,所以我们可以在 Queryset 中使用它。

    def get_descendant_count(self):
        """
        Returns the number of descendants this model instance has.
        """
        if self._mpttfield('right') is None:
            # node not saved yet
            return 0
        else:
            return (self._mpttfield('right') - self._mpttfield('left') - 1) // 2
    

    这是当前 mptt 模型的方法。在查询集中,我们确定所有实例都已保存,因此我们将跳过。

    下一步是将数学运算转换为数据库表达式。 在 Django 3.0 中出现了Floorexpression。但是我们甚至可以在 1.7 中使用它(就像我一样)

    from django.db.models.lookups import Transform
    
    class Floor(Transform):
         function = 'FLOOR'
         lookup_name = 'floor'
    

    如果您愿意,可以将其重构为使用 self._mpttfield('right') 模拟而不是硬编码的 rght, lft 并将其设为 Manager 方法

    让我们测试一下。我有后代的顶级元素

    In [1]: m = MenuItem.objects.get(id=settings.TOP_MENU_ID)
    
    In [2]: m.get_descendant_count()
    Out[2]: 226
    
    In [3]: n = m.get_descendants()
    
    In [4]: n.annotate(descendants_count=Floor((F('rght') - F('lft') - 1) / 2)).values('descendants_count').aggregate(total_count=Count('descendants_count'))
    Out[4]: {'total_count': 226}
    

    【讨论】:

    • 非常感谢@Andrey 的精彩而完整的回答。在查询单个 cmets 时,我得到了这个工作,但不幸的是,我无法从 Page queryset object-> Page.objects.all()[...].annotate(descendent_counts) 中弄清楚如何做到这一点。这样我就会得到一个“页面”列表,其中包含与该页面关联的所有 cmets 的总后代数。这会进入子查询吗?我认为在加入 Page -> Thread -> Comments 之间有一个步骤
    • @gtalarico 您只需要获取您的Comment 过滤器查询集,例如comments = Comment.objects.filter(thread=page.thread)
    • 再次感谢。我认为这比这要复杂一些,因为页面和评论是由评论线程连接的。感谢您的帮助,我终于能够得到我需要的结果,但不确定它是否是最佳的。无论如何,现在已经足够快了。我已更新我的答案以包含解决方案,
    猜你喜欢
    • 2020-05-20
    • 2019-01-05
    • 1970-01-01
    • 1970-01-01
    • 2015-10-01
    • 2012-10-12
    • 1970-01-01
    • 1970-01-01
    • 2017-03-13
    相关资源
    最近更新 更多