文中所涉及的示例代碼,已同步更新到 HelloGitHub-Team 倉庫
在我們的博客側邊欄有分類列表和標簽列表,顯示博客已有的全部文章分類。現在想在分類名和標簽名后顯示該分類或者標簽下有多少篇文章,該怎么做呢?最優雅的方式就是使用 django 的 annotate
方法。
Model 回顧
回顧一下我們的 model 代碼,django 博客有一個 Post
和 Category
模型,分別表示文章和分類:
blog/models.py
class Post(models.Model):
title = models.CharField('標題', max_length=70)
body = models.TextField('正文')
category = models.ForeignKey(Category, verbose_name='分類', on_delete=models.CASCADE)
# 其它屬性...
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
方法,該方法正可以幫我們實現本文所關注的統計分類下的文章數量的功能。
數據庫數據聚合
annotate
方法在底層調用了數據庫的數據聚合函數,下面使用一個實際的數據庫表來幫助我們理解 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 id 對應有多少行記錄,這樣就可以統計出每個 Category 下有多少篇文章了。把這個統計數字保存到每一條 Category 的記錄就可以了(當然並非保存到數據庫,在 Django ORM 中是保存到 Category 的實例的屬性中,每個實例對應一條記錄)。
使用 Annotate
以上是原理方面的分析,具體到 Django 中該如何用呢?在我們的博客中,獲取側邊欄的分類列表的方法寫在模板標簽 get_categories
里,因此我們修改一下這個函數,具體代碼如下:
blog/templatetags/blog_extras.py
from django.db.models.aggregates import Count
from blog.models import Category
@register.inclusion_tag('blog/inclusions/_categories.html', takes_context=True)
def show_categories(context):
category_list = Category.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
return {
'category_list': category_list,
}
這個 Category.objects.annotate
方法和 Category.objects.all
有點類似,它會返回數據庫中全部 Category 的記錄,但同時它還會做一些額外的事情,在這里我們希望它做的額外事情就是去統計返回的 Category 記錄的集合中每條記錄下的文章數。代碼中的 Count
方法為我們做了這個事,它接收一個和 Categoty 相關聯的模型參數名(這里是 Post
,通過 ForeignKey 關聯的),然后它便會統計 Category 記錄的集合中每條記錄下的與之關聯的 Post 記錄的行數,也就是文章數,最后把這個值保存到 num_posts
屬性中。
此外,我們還對結果集做了一個過濾,使用 filter
方法把 num_posts
的值小於 1 的分類過濾掉。因為 num_posts
的值小於 1 表示該分類下沒有文章,沒有文章的分類我們不希望它在頁面中顯示。關於 filter
函數以及查詢表達式(雙下划線)在之前已經講過,具體請參考 分類、歸檔和標簽頁。
同理,tags 也可以做同樣的操作。
@register.inclusion_tag('blog/inclusions/_tags.html', takes_context=True)
def show_tags(context):
tag_list = Tag.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
return {
'tag_list': tag_list,
}
在模板中引用新增的屬性
現在在 Category 和 Tag 列表中每一項都新增了一個 num_posts
屬性記錄該 Category 下的文章數量,我們就可以在模板中引用這個屬性來顯示分類下的文章數量了。
templates/blog/inclusions/_categories.html
<div class="widget widget-category">
<h3 class="widget-title">分類</h3>
<ul>
{% for category in category_list %}
<li>
<a href="{% url 'blog:category' category.pk %}">{{ category.name }} <span class="post-count">({{ category.num_posts }})</span></a>
</li>
{% empty %}
暫無分類!
{% endfor %}
</ul>
</div>
標簽也是一樣:
templates/blog/inclusions/_tags.html
<div class="widget widget-tag-cloud">
<h3 class="widget-title">標簽雲</h3>
<ul>
{% for tag in tag_list %}
<li>
<a href="{% url 'blog:tag' tag.pk %}">{{ tag.name }} <span class="post-count">({{ tag.num_posts }})</a>
</li>
{% empty %}
暫無標簽!
{% endfor %}
</ul>
</div>
也就是在模板中通過模板變量 {{ category.num_posts }}
顯示 num_posts
的值。開啟開發服務器,可以看到分類名后正確地顯示了該分類下的文章數了,而沒有文章分類則不會在分類列表中出現。
『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟着我們的文章,你會發現編程的樂趣、使用和發現參與開源項目如此簡單。歡迎留言聯系我們、加入我們,讓更多人愛上開源、貢獻開源~