注释 Mptt 模型的下降总数
Annotate Total Count of Descents of Mptt Model
问题
给定以下模型,我想获取所有页面的查询集,并用与页面关联的线程中的评论总数进行注释,包括与页面关联的评论线程树中的所有评论。
我正在使用 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']
这个模型允许我向页面添加多个“根评论”,而且还可以递归地将评论嵌套在每个评论下作为回复。
# 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
,所以我猜我需要构建一个更复杂的子查询。
为了只获取根评论的数量(不包括嵌套),我可以这样做:
pages = Page.objects.all().annotate(num_comments=models.Count("thread__comments"))
num_root_comments = pages[0].num_comments
再一次,目标是获取所有评论,包括嵌套:
# 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 的回答,找到了可行的解决方案。
不确定它是否最佳,但似乎 return 单个查询中的正确值。
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)
)
)
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 中出现了 Floor
expression。但是我们甚至可以在 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}
问题
给定以下模型,我想获取所有页面的查询集,并用与页面关联的线程中的评论总数进行注释,包括与页面关联的评论线程树中的所有评论。
我正在使用 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']
这个模型允许我向页面添加多个“根评论”,而且还可以递归地将评论嵌套在每个评论下作为回复。
# 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
,所以我猜我需要构建一个更复杂的子查询。
为了只获取根评论的数量(不包括嵌套),我可以这样做:
pages = Page.objects.all().annotate(num_comments=models.Count("thread__comments"))
num_root_comments = pages[0].num_comments
再一次,目标是获取所有评论,包括嵌套:
# 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 的回答,找到了可行的解决方案。 不确定它是否最佳,但似乎 return 单个查询中的正确值。
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)
)
)
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 中出现了 Floor
expression。但是我们甚至可以在 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}