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 --


22 条评论 / 8 人参与
chenyufei91
博主你好 我用annotate的方法取值 归档数量 发现怎么取都取不到值 全都是空。其他的几个分类数量我都取到了,就是卡在归档这两天了,能否告知下 归档下的annotate的取值怎么取。不胜感激~~

追梦人物 [博主] chenyufei91
没有研究过归档的这种方法。

chenyufei91 追梦人物 [博主]
我看你之前跟一个人说过可以用annotate方法用自定义标签查询出归档每个月的文章数量,你跟他说留给他自己实现,看model层文档。我在文档文档也没说这种情况- - 。我用Post.object.date(xxxx).annotate(num_post=Count('') )取到的都是空,是不是我这句话本身方法就有问题?.date返回的是一个列表,是不是在后面不能用annotate?
你文章里说只要是两个 model 类通过 ForeignKey 或者 ManyToMany 关联起来,那么就可以使用 annotate 方法来统计数量。
是不是date没有跟Post关联起来就用不了annotate?

追梦人物 [博主] chenyufei91
估计我忘了,现在找到解决方案了,但我估计用 annotate 可能无法简单实现。我有另外一种实现方式,过几天会发布一篇博客文章出来,到时候你看看。

吉超 chenyufei91

请问你解决了吗


dackzome
1、修改【blog_tags.py】
#分类模板标签
@register.simple_tag()
def get_categories():
category_list = Category.objects.annotate(num_posts=Count('post'))
# return Category.objects.all()
return category_list
2、【base.html】

分类


{% get_categories as category_list %}


【看评论有些人不知道添加到什么地方,我标出来了】

chenyang929
@register.simple_tag
def get_categories():
+category_list = Category.objects.annotate(num_article=Count('article'))
-return Category.objects.all()
+return category_list
摸索了一会,原来要写在模板标签里,建议博主这个地方可以写的详细点。另外忍不住赞一下博主,教程实在写的太好了。

yeliang
博主,您好,使用{{ category.post_set.count }}这个会不会更加方便简洁一些吗?不然为了这个还要做一个模板标签

追梦人物 [博主] yeliang
这也是可以的,不过这样的问题是每调用一次 category.post_set.coun 就需要查询一次数据库,而文中的方法只需查询一次。此外我们要过滤掉文章数为 0 的分类,所以感觉用 annotate 更加优雅。

yeliang 追梦人物 [博主]
非常感谢作者的解释

Jenny的太空漫游
博主你好~,跟着你的教程把代码写了下来,在这里遇到一个有点困惑的地方,还想向博主请教一下。
我修改了模板标签的代码,但是网页上还是不能显示出统计数量,是不是统计数量的代码里的category_list和之前用来设置目录的{% get_categories as category_list %}中的category_list代码造成了冲突?

追梦人物 [博主] Jenny的太空漫游
能否把模板标签的代码贴出来看一下?

Jenny的太空漫游 追梦人物 [博主]
blog/views.py

def category(request,pk):
cate=get_object_or_404(Category,pk=pk)
category_list=Category.objects.annotate(num_posts=Count('post'))
post_list=Post.objects.filter(category=cate).order_by('-created_time')
return render(request,'blog/index.html',context={'post_list':post_list,
'category_list':category_list,
})

----------------------------------------------------------------------------------

base.html
{% get_categories as category_list %}

追梦人物 [博主] Jenny的太空漫游
你弄错了,由于过去分类列表是使用的模板标签,所以获取分类下文章数量的代码也应该写在模板标签里,而不是写在视图函数里。我们从来没从视图函数中获取过分类列表。

Jenny的太空漫游 追梦人物 [博主]
博主,我是真的不懂category_list=Category.objects.annotate(num_posts=Count('post'))这段代码该放哪里?放在blog/templatetags/blog_tags.py的下吗?
而base.html里的 {% for category in category_list %}又该怎么处理呢?

追梦人物 [博主] Jenny的太空漫游
是的,base 的 category list 就是模板标签获得的。

我是单永旭
博主,将({{ category.num_posts }})放在base.html无法显示,放在index.html就可以正常统计数量,这是什么原因呢?blog/views.py里面index函数的是
def index(request):
post_list = Post.objects.all().order_by('-created_time')
category_list = Category.objects.annotate(num_posts=Count('post'))
return render(request, 'blog/index.html', context={'post_list': post_list, 'category_list': category_list})

追梦人物 [博主] 我是单永旭
因为我们的博客项目里面处理侧边栏分页用的是模板标签,不是 index 视图处理的,所以你要修改模板标签的代码,而不是主页视图函数的代码

我是单永旭 追梦人物 [博主]
放在模板页面base.html没有效果...

追梦人物 [博主] 我是单永旭
看一下模板标签 category 的代码。

我是单永旭 追梦人物 [博主]
{%for category in category_list%}

  • {{category.name}}({{ category.num_posts}})

  • %endfor%}

    追梦人物 [博主] 我是单永旭
    你如果在 index 函数处理 category,base 模板是接收不到 category list 的,所以我们才使用自定义模板标签。具体请看自定义模板标签那一部分的教程。
    目录