Django 數據聚合函數:annotate

統計各個分類下的文章數

在我們的博客側邊欄有分類列表,顯示博客已有的全部文章分類。現在想在分類名後顯示該分類下有多少篇文章,該怎麼做呢?最優雅的方式就是使用 Django 模型管理器的 annotate 方法。

模型回顧

回顧一下我們的模型代碼,Django 博客有一個 Post 和 Category 模型,分別表示文章和分類:

blog/models.py

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 方法,該方法正可以幫我們實現本文所關注的統計分類下的文章數量的功能。

數據庫數據聚合

annotate 方法在底層調用了數據庫的數據聚合函數,下面使用一個實際的數據庫表來幫助我們理解 annotate 方法的工作原理。在 Post 模型中我們通過 ForeignKey 把 Post 和 Category 關聯了起來,這時候它們的數據庫表結構就像下面這樣:
Post表:

id title category_id
1 post-1 1
2 post-2 1
3 post-3 2

Category表:

id name
1 name-1
2 name-2

這裏前 2 篇文章屬於 category 1,第 3篇文章屬於 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,發現前 2 行都是,把這些行取出來就是 category 1 下的全部文章了。同理,這裏 annotate 做的事情就是把全部 Category 取出來,然後去 Post 查詢每一個 Category 對應的文章,查詢完成後只需算一下每個 category id 對應有多少行記錄,這樣就可以統計出每個 Category 下有多少篇文章了。把這個統計數字保存到每一條 Category 的記錄就可以了(當然並非保存到數據庫,在 Django ORM 中是保存到 Category 的實例的屬性中,每個實例對應一條記錄)。

使用annotate

blog/templatetags/blog_tags.py

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

@register.simple_tag
def get_categories():
    # 記得在頂部引入 count 函數
    # Count 計算分類下的文章數,其接受的參數爲需要計數的模型的名稱
    return Category.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)

這個 Category.objects.annotate 方法和 Category.objects.all 有點類似,它會返回數據庫中全部 Category 的記錄,但同時它還會做一些額外的事情,在這裏我們希望它做的額外事情就是去統計返回的 Category 記錄的集合中每條記錄下的文章數。代碼中的 Count 方法爲我們做了這個事,它接收一個和 Categoty 相關聯的模型參數名(這裏是 Post,通過 ForeignKey 關聯的),然後它便會統計 Category 記錄的集合中每條記錄下的與之關聯的 Post 記錄的行數,也就是文章數,最後把這個值保存到 num_posts 屬性中。

此外,我們還對結果集做了一個過濾,使用 filter 方法把 num_posts 的值小於 1 的分類過濾掉。因爲 num_posts 的值小於 1 表示該分類下沒有文章,沒有文章的分類我們不希望它在頁面中顯示。關於 filter 函數以及查詢表達式(雙下劃線)在之前已經講過,具體請參考分類與歸檔。

在模板中引用新增的屬性

現在在 Category 列表中每一項都新增了一個 num_posts 屬性記錄該 Category 下的文章數量,我們就可以在模板中引用這個屬性來顯示分類下的文章數量了。

templates/base.html

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

也就是在模板中通過模板變量 {{ category.num_posts }} 顯示 num_posts 的值。開啓開發服務器,可以看到分類名後正確地顯示了該分類下的文章數了,而沒有文章分類則不會在分類列表中出現。

將annotate用於其他關聯關係

此外,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 Tag

# Count 計算分類下的文章數,其接受的參數爲需要計數的模型的名稱
tag_list = Tag.objects.annotate(num_posts=Count('post'))

關於 annotate 方法官方文檔的說明在這裏:annotate。同時也建議瞭解瞭解 objects 下的其它操作數據庫的方法,以便在遇到相關問題時知道去哪裏查閱。

發佈了115 篇原創文章 · 獲贊 52 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章