Django使用相关查询集的第一个元素进行注释

18 投票
2 回答
12468 浏览
提问于 2025-04-18 06:23

问题

我正在为一个简单的论坛创建数据库模型。用户应该能够创建主题、添加帖子,并在帖子中上传图片。

在一个视图中,我想显示所有主题,并且:

  • 获取主题中第一个帖子的字段,以显示帖子的一部分内容、创建日期等(包括可选的图片)
  • 获取主题中最后一个帖子的时间
  • 统计主题中的帖子数量
  • 统计主题中的图片数量

我觉得如果要对每个主题执行 n 次查询,这几乎是不可能的,所以真正的问题是如何重新设计数据库以实现这一点。

class Thread(models.Model):
    sticky = models.BooleanField()
    ...

class Post(models.Model):
    thread = models.ForeignKey('Thread')
    image = models.OneToOneField('Image', null=True, blank=True, default=None)
    date = models.DateTimeField()
    ...

class Image(models.Model):
    image = models.ImageField(...)
    ...

我的部分解决方案

目前我知道如何统计帖子和图片,但我不知道如何同时获取第一个帖子。我考虑在 Thread 模型中添加一个额外的字段,链接到第一个 Post

我的查询迫使我单独下载第一个帖子:

Thread.objects.annotate(
    replies=Count('post'),
    images=Count('post__image'),
    last_reply=Max('post_date')
)

2 个回答

0

你需要的部分是 select_related。你还需要像你预想的那样使用 annotate

# I assume you have thread_id given to you.
last_reply = Post.objects.annotate(
    thread_images=Count('thread__post_set__image__id', distinct=True),
    replies=Count('thread__post_set__id', distinct=True),
).select_related('thread').filter(thread__id=thread_id).order_by('-post_date').first()
18

你可以使用一个子查询来从最近的相关对象中提取单个字段的信息:

comments = Comment.objects.filter(
    post=OuterRef('pk')
).order_by('-timestamp').values('timestamp')
Post.objects.annotate(
    last_comment_time=Subquery(comments[:1])
)

你也可以用这种方式提取多个字段,但这样会影响性能(每个相关的子查询都是单独运行的,对于每一行来说,这比N+1个查询要好,但比一个单独的连接要差)。

你可以在一个字段上构建一个JSON对象,然后将其添加到:

comments = Comment.objects.filter(
    post=OuterRef('pk')
).annotate(
    data=models.expressions.Func(
        models.Value('author'), models.F('author'),
        models.Value('timestamp'), models.F('timestamp'),
        function='jsonb_build_object',
        output_field=JSONField()
    ),
).order_by('-timestamp').values('data')

(其实也可以把整个对象作为JSON获取,然后在Django中再重新解析,但这有点不太规范)。


另一种解决方案是单独获取最近的评论,然后将它们和帖子结合起来:

comments = Comment.objects.filter(
    ...
).distinct('post').order_by('post', '-timestamp')
posts = Post.objects.filter(...).order_by('pk')

for post, comment in zip(posts, comments):
    pass

你需要确保帖子和评论的顺序是一致的:这些查询就是这样做的。如果某个帖子没有评论,这样的做法就会失败。

解决这个问题的一种方法是把评论放到一个以帖子ID为键的字典中,然后为每个帖子获取匹配的评论。

comments = {
    comment.post_id: comment
    for comment in Comment.objects.distinct('post').order_by('post', '-timestamp')
}
for post in Post.objects.filter(...):
    top_comment = comments.get(post.pk)
    # whatever

撰写回答