Django Blog 统计某个分类下有多少篇文章的优雅实现方法

假设我们有如下的 Model :

class Article(models.Model):
    title = models.CharField('标题', max_length=200)
    body = models.TextField('正文')
    created_time = models.DateTimeField('创建时间', auto_now_add=True)
    
    author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='作者', on_delete=models.CASCADE)
    category = models.ForeignKey('Category', verbose_name='分类', on_delete=models.CASCADE)
    tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True)

    def __str__(self):
        return self.title

class Category(models.Model):
    name = models.CharField('分类名', max_length=30)

    def __str__(self):
        return self.name

class Tag(models.Model):
    name = models.CharField('标签名', max_length=30)

    def __str__(self):
        return self.name

可以看到 Article 与 Author,Category 是外键关联的,而和 Tag 多对多的关系。有时候我们有这样的需求:在模板中显示全部的 category,author,tag,同时还要对应显示与其关联的 Article 数量,例如:

  • 分类一 ( 10 )
  • 分类二 ( 15 )
  • 分类三 ( 8 )

由于 Article 和 Category 外键关联的,在模板中我们可以使用 {{ category_instance.article_set.count }} 取得 category_instance 这一特定分类下关联的全部 Article 数量,但是缺点也很明显,无法在模板标签中传递参数,count 方法将返回全部关联的文章数目。有时我们想要更加精细化一点的显示,比如统计某个分类下全部 Article 发表时间在某个时间点后的文章,而如果该分类对应的文章数为 0 ,我们在模板中则不显示该分类。看上去挺复杂的逻辑,Django 的 annote 方法一句话可以解决我们的问题。

使用:

from django.db.models.aggregates import Count

category_list = Category.objects.filter(article__created_time_gt=(2015,1,1)).annotate(
    num_articles=Count('article')).filter(num_articles__gt=0)

annote 的功能是根据某个规则给 queryset 中的每一个元素(例如我们这里的是 category)添加一个属性。queryset 是从数据库中查询到的一组数据的集合,Django 将其封装在 QuerySet 对象里。

这句话的意思的,首先对 Category 做筛选(filter),即筛选出其对应的全部文章发表时间大于 2015年1月1日的 category记录(article__created_time_gt=(2015,1,1)) ,然后为筛选出来的每一个条 category 记录添加了一个属性:num_articles(num_articles=Count('article'))),Count 方法为我们计算了每一条 catogory 下对应的 article 数量,最后再对这组记录筛选出对应文章数量大于 0 的记录,即满足了我们上述要求。同理可以对 Author,Tag 做类似筛选。

值得注意的是第一个 filter 和 annotate 的顺序不能乱,否则可能无法得到我们预期的结果。

相关文档位于:QuerySet Method referenceAggregation

灵活使用这些方法将使我们复杂的查询需求代码变得更加高效和简短。

推荐阅读更多精彩内容