django 博客使用 annotate 统计分类下文章数量

博客文章通常都有分类,有时候我们会看到分类名后面还跟着该分类下的文章数量。前面我们通过学习 django 博客开发入门教程搭建了一个小博客。现在想在现有的基础上实现统计分类下有多少篇文章,该怎么做呢?最优雅的方式就是使用 django 模型的 annotate 方法。

假设我们的 django 博客有一个 Post 和 Category 模型,分别表示文章和分类:

class Post(models.Model):
    title = models.CharField(max_length=70)
    body = models.TextField()
    category = models.ForeignKey('Category')

    def __str__(self):
        return self.title

class Category(models.Model):
    name = models.CharField(max_length=100)

我们知道从数据库取数据都是使用模型管理器 objects 实现的。比如获取全部分类是:Category.objects.all() ,假设有一个名为 test 的分类,那么获取该分类的方法是:Category.objects.get(name='test') 。objects 除了 all、get 等方法外,还有很多操作数据库的方法,而其中有一个 annotate 方法,该方法正可以帮我们实现本文所关注的统计分类下的文章数量的功能。具体来说,就是如下的代码:

from django.db.models.aggregates import Count
from blog.models import Category

# Count 计算分类下的文章数,其接受的参数为需要计数的模型的名称
category_list = Category.objects.annotate(num_posts=Count('post'))

这里 annotate 不仅从数据库获取了全部分类,相当于使用了 all 方法,它还帮我们为每一个分类添加了一个 num_posts 属性,其值为该分类下的文章数,这样我们在模板中就可以调用这个属性,例如:

{% for category in category_list %}
  <li>
    <a href="{% url 'blog:category' category.pk %}">
      {{ category.name }} ({{ category.num_posts }})</a>
  </li>
{% endfor %}

这样显示的效果就是分类名后跟着该分类下的文章数了。

那么 annotate 的工作原理究竟是怎么样的呢?在 Post 模型中我们通过 ForeignKey 把 Post 和 Category 关联了起来,这时候它们的数据库表结构就像下面这样:

Post 表:

id title body category_id
1 post 1 ... 1
2 post 2 ... 1
3 post 3 ... 1
4 post 4 ... 2

Category 表:

name id
category 1 1
category 2 2

这里前 3 篇文章属于 category 1,第 4 篇文章属于 category 2。

当 django 要查询某篇 post 对应的分类时,比如 post 1,首先查询到它分类的 id 为 1,然后 django 再去 Category 表找到 id 为 1 的那一行,这一行就是 post 1 对应的分类了。反过来,如果要查询 category 1 对应的全部文章呢?category 1 在 Category 表中对应的 id 是 1,django 就在 Post 表中搜索哪些行的 category_id 为 1,发现前 3 行都是,把这些行取出来就是 category 1 下的全部文章了。同理,这里 annotate 做的事情就是把全部 Category 取出来,然后去 Post 查询每一个 Category 对应的文章,查询完成后做一个聚合,统计每个 Category 有多少篇文章,把这个统计数字保存到 Category 的 num_posts 属性里(注意 Category 本身没有这个属性,是 Python 动态添加上去的)。

此外,annotate 方法不局限于用于本文提到的统计分类下的文章数,你也可以举一反三,只要是两个 model 类通过 ForeignKey 或者 ManyToMany 关联起来,那么就可以使用 annotate 方法来统计数量。比如下面这样一个标签系统:

class Post(models.Model):
    title = models.CharField(max_length=70)
    body = models.TextField()
    Tags = models.ManyToMany('Tag')

    def __str__(self):
        return self.title

class Tag(models.Model):
    name = models.CharField(max_length=100)

统计标签下的文章数:

from django.db.models.aggregates import Count
from blog.models import Category

# Count 计算分类下的文章数,其接受的参数为需要计数的模型的名称
category_list = Category.objects.annotate(num_posts=Count('post'))

关于 annotate 方法官方文档的说明在这里:annotate。同时也建议了解了解 objects 下的其它操作数据库的方法,以便在遇到相关问题时知道去哪里查阅。

-- EOF --