【问题标题】:Django annotating with a first element of a related querysetDjango 使用相关查询集的第一个元素进行注释
【发布时间】:2014-07-01 00:28:26
【问题描述】:

问题

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

在一个视图中我想显示所有线程并且:

  • 获取线程中第一个帖子的字段以显示帖子/创建日期等的一部分(包括可选图片)
  • 获取线程中最后一个帖子的时间
  • 统计线程中的帖子
  • 计算线程中的图像数

我相信如果不对n 线程执行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(...)
    ...

我的部分解决方案

此时我知道如何计算帖子和图像,但我不知道如何同时获取第一个帖子。我考虑在链接到第一个PostThread 模型中添加额外的字段。

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

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

【问题讨论】:

标签: python django django-models


【解决方案1】:

您可以使用Subquery 在最近相关对象的单个字段上进行注释:

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 中重新膨胀,但这有点 hacky)。


另一种解决方案是分别获取最新的 cmets,然后将它们与帖子合并:

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

您需要确保帖子和 cmets 在此处的顺序相同:这些查询是。如果每个帖子都没有评论,这也会失败。

一种解决方法可能是将 cmets 放入以帖子 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

【讨论】:

    【解决方案2】:

    您正在寻找的部分内容是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()
    

    【讨论】:

    • 这并不是我希望完成的。有没有办法选择更多的记录?正如我所说,在显示所有创建的线程列表的同时显示线程中第一个帖子的预览会很棒。此解决方案迫使我按 thread_id 过滤,从而只得到一个结果。
    • 您可以获取每个线程的单个帖子并将两个集合压缩在一起。
    猜你喜欢
    • 2019-12-30
    • 2016-09-22
    • 1970-01-01
    • 1970-01-01
    • 2020-07-31
    • 1970-01-01
    • 2020-11-14
    • 2019-04-03
    • 1970-01-01
    相关资源
    最近更新 更多