第 9 篇:實現分類、標籤、歸檔日期介面

削微寒發表於2020-06-12

作者:HelloGitHub-追夢人物

我們的部落格有一個側邊欄功能,分別列出部落格文章的分類列表、標籤列表、歸檔時間列表,通過點選側邊欄對應的條目,還可以進入相應的頁面。例如點選某個分類,部落格將跳轉到該分類下全部文章列表頁面。這些資料的展示都需要開發對應的介面,以便前端呼叫獲取資料。

分類列表、標籤列表實現比較簡單,我們這裡給出介面的設計規範,大家可以使用前幾篇教程中學到的知識點輕鬆實現(具體實現可參考 GtiHub 上的原始碼)。

分類列表介面: /categories/

標籤列表介面:/tags/

歸檔日期列表的介面實現稍微複雜一點,因為我們需要從已有文章中歸納文章發表日期。事實上,我們在上一部教程 HelloDjango - Django部落格教程(第二版)的 頁面側邊欄:使用自定義模板標籤 已經講解了如何獲取歸檔日期列表,只是當時返回的歸檔日期列表直接用於模板的渲染,而這裡我們需要將歸檔日期列表序列化後通過 API 介面返回。

具體來說,獲取部落格文章發表時間歸檔列表的方法是呼叫查詢集(QuerySet)的 dates 方法,提取記錄中的日期。核心程式碼就一句:

Post.objects.dates('created_time', 'month', order='DESC')

這裡 Post.objects.dates 方法會返回一個列表,列表中的元素為每一篇文章(Post)的建立日期(已去重),日期都是 Python 的 date 物件,精確到月份,降序排列。

有了返回的歸檔日期列表,接下來就實現相應的 API 介面檢視函式:

blog/views.py

from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.serializers import DateField

class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
	# ...

    @action(
        methods=["GET"], detail=False, url_path="archive/dates", url_name="archive-date"
    )
    def list_archive_dates(self, request, *args, **kwargs):
        dates = Post.objects.dates("created_time", "month", order="DESC")
        date_field = DateField()
        data = [date_field.to_representation(date) for date in dates]
        return Response(data=data, status=status.HTTP_200_OK)

注意這裡我們涉及到了幾個以前沒有詳細講解過的用法。

一是 action 裝飾器,它用來裝飾一個檢視集中的方法,被裝飾的方法會被 django-rest-framework 的路由自動註冊為一個 API 介面。

回顧一下我們之前在使用檢視集 viewset 時提到過 action(動作)的概念,django-rest-framework 預定義了幾個標準的動作,分別為 list 獲取資源列表,retrieve 獲取單個資源、update 和 partial_update 更新資源、destroy 刪除資源,這些 action 具體的實現方法,分別由 mixins 模組中的混入類提供。例如 用類檢視實現首頁 API 中我們介紹過 mixins.ListModelMixin,這個混入類提供了 list 動作對應的標準實現,即 list 方法。檢視集中所有以上提及的以標準動作命名的方法,都會被 django-rest-framework 的路由自動註冊為標準的 API 介面。

django-rest-framework 預設只能識別標準命名的檢視集方法並將其註冊為 API,但我們可以新增更多非標準的 action,而為了讓 django-rest-framework 能夠識別這些方法,就需要使用 action 裝飾器進行裝飾。

其實我們可以簡單地將 action 裝飾的方法看作是一個檢視函式的實現,因此可以看到方法傳入的第一個引數為 request 請求物件,函式體就是這個檢視函式需要執行的邏輯,顯然,方法最終必須要返回一個 HTTP 響應物件。

action 裝飾器通常用於在檢視集中新增額外的介面實現。例如這裡我們已有了 PostViewSet 檢視集,標準的 list 實現了獲取文章資源列表的邏輯。我們想新增一個獲取文章歸檔日期列表的介面,因此新增了一個 list_archive_dates 方法,並使用 action 進行裝飾。通常如果要在檢視集中新增額外的介面實現,可以使用如下的模板程式碼:

@action(
    methods=["allowed http method name"], 
    detail=False or True, 
    url_path="url/path", 
    url_name="url name"
)
def method_name(self, request, *args, **kwargs):
    # 介面邏輯的具體實現,返回一個 Response

通常 action 裝飾器以下 4 個引數都會設定:

methods:一個列表,指定訪問這個介面時允許的 HTTP 方法(GET、POST、PUT、PATCH、DELETE)

detail:True 或者 False。設定為 True,自動註冊的介面 URL 中會新增一個 pk 路徑引數(請看下面的示例),否則不會。

url_path:自動註冊的介面 URL。

url_name:介面名,主要用於通過介面名字反解對應的 URL。

當然,我們還可以在 action 中設定所有 ViewSet 類所支援的類屬性,例如 serializer_classpagination_classpermission_classes 等,用於覆蓋類檢視中設定的屬性值。

以上是 action 用法的一個基本介紹,現在來分析一下 list_archive_dates 這個 action 來加深理解。

methods 引數指定介面需要通過 GET 方法訪問,detail 為 Falseurl_path 設定為 archive/dates,因此最終自動生成的介面路由就是 /posts/archive/dates/。如果我們設定 detail 為 True,那麼生成的介面路由就是 /posts/<int:pk>/archive/dates/,生成的 URL 中就會多一個 pk 路徑引數。

list_archive_dates 具體的實現邏輯中,以下幾點需要注意:

一是獨立使用序列化欄位(Field)。之前序列化欄位都是在序列化器(Serializer)裡面使用的,因為通常來說介面需要序列化一個物件的多個欄位。而這個介面中只需要序列化一個時間欄位(型別為 Python 標準庫中的 datetime.date),所以沒必要單獨定義一個序列化器了,直接拿 django-rest-framework 提供的用於序列化時間型別的 DateField 就可以了。用法也很簡單,例項化序列化欄位,呼叫其 to_representation 方法,將需要序列化的值傳入即可(其實序列化器在序列物件的多個欄位時,內部也是分別呼叫對應序列化欄位的 to_representation 方法)。

我們通過列表推導式生成一個序列化後的歸檔日期列表,這個列表是可被序列化的。接著我們在介面返回一個 ResponseResponse 將序列化後的結果包裝返回(儲存在 data 屬性中),django-rest-framework 會進一步幫我們把這個 Response 中包含的資料解析為合適的格式(例如 JSON)。

status=status.HTTP_200_OK 指定這個介面返回的狀態碼,HTTP_200_OK 是一個預定義的常數,即 200。django-rest-framework 將常用 HTTP 請求的狀態碼常數預定義 status 模組裡,使用預定義的變數而不是直接使用數字的好處一是增強程式碼可讀性,二是減少硬編碼。

由於 PostViewSet 檢視集已經通過 django-rest-framework 的路由進行了註冊,因此 list_archive_dates 也會被連帶著自動註冊為一個介面。啟動開發伺服器,訪問 /posts/archive/dates/,就可以看到返回的文章歸檔日期列表。

![文章歸檔日期返回結果](https://blog-1253812787.cos.ap-chengdu.myqcloud.com/

.png)

注意到紅框圈出部分,django-rest-framework API 互動後臺會識別到額外定義的 action 並將它們展示出來,點選就可以進入到相應的 API 頁面。

現在,側邊欄所需要的資料介面就開發完成了,接下來實現返回某一分類、標籤或者歸檔日期下的文章列表介面。

使用檢視集簡化程式碼 我們開發了獲取全部文章的介面。事實上,分類、標籤或者歸檔日期文章列表的 API,本質上還是返回一個文章列表資源,只不過比首頁 API 返回的文章列表資源多了個“過濾”,只過濾出了指定的部分文章而已。對於這樣的場景,我們可以在請求 API 時加上查詢引數,django-rest-framework 解析查詢引數,然後從全部文章列表中過濾出查詢所指定的文章列表再返回。

這在 RESTful API 的設計中肯定是會遇到的,因此第三方庫 django-filter 幫我們實現了上述所說的查詢過濾功能,而且和 django-rest-framework 有很好的整合,我們可以在 django-rest-framework 中非常方便地使用 django-filter。

既然要使用它,當然是先安裝它(已安裝跳過):pipenv install django-filter

接著我們來配置 PostViewSet,為其設定用於過濾返回結果集的一些屬性,程式碼如下:

from django_filters.rest_framework import DjangoFilterBackend
from .filters import PostFilter

class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    # ...
    filter_backends = [DjangoFilterBackend]
    filterset_class = PostFilter

非常的簡單,僅僅設定了 filter_backendsfilterset_class 兩個屬性。其中 filter_backends 設定為 DjangoFilterBackend,這樣 API 在返回結果時, django-rest-framework 會呼叫設定的 backend(這裡是 DjangoFilterBackend) 的 filter 方法對 get_queryset 方法返回的結果進行進一步的過濾,而 DjangoFilterBackend 會依據 filterset_class(這裡是 PostFilter)中定義的過濾規則來過濾查詢結果集。

當然 PostFilter 還沒有定義,我們來定義它。首先在 blog 應用下建立一個 filters.py 檔案,用於存放自定義 filter 的程式碼,PostFilter 程式碼如下:

from django_filters import rest_framework as drf_filters

from .models import Post


class PostFilter(drf_filters.FilterSet):
    created_year = drf_filters.NumberFilter(
        field_name="created_time", lookup_expr="year"
    )
    created_month = drf_filters.NumberFilter(
        field_name="created_time", lookup_expr="month"
    )

    class Meta:
        model = Post
        fields = ["category", "tags", "created_year", "created_month"]

PostFilter 的定義和序列化器 Serializer 非常類似。

categorytags 兩個過濾欄位因為是 Post 模型中定義的欄位,因此 django-filter 可以自動推斷其過濾規則,只需要在 Meta.fields 中宣告即可。

歸檔日期下的文章列表,我們設計的介面傳遞 2 個查詢引數:年份和月份。由於這兩個欄位在 Post 中沒有定義,Post 記錄時間的欄位為 created_time,因此我們需要顯示地定義查詢規則,定義的規則是:

查詢引數名 = 查詢引數值的型別(查詢的模型欄位,查詢表示式)

例如示例中定義的 created_year 查詢引數,查詢引數值的型別為 number,即數字,查詢的模型欄位為 created_time,查詢表示式是 year。當使用者傳遞 created_year 查詢引數時,django-filter 實際上會將以上定義的規則翻譯為如下的 ORM 查詢語句:

Post.objects.filter(created_time__year=created_year傳遞的值)

現在回到 API 互動後臺,先進到 /post/ 介面下,預設返回了全部文章列表。可以看到右上角多了個過濾器(紅框圈出部分)。

點選會彈出過濾引數輸入的互動皮膚,在這裡可以互動式地輸入查詢過濾引數的值。

例如選擇如下的過濾引數,得到查詢的 URL 為:

http://127.0.0.1:10000/api/posts/?category=1&tags=1&created_year=2020&created_month=1

這條查詢返回建立於 2020 年 1 月,id 為 1 的分類下,id 為 1 的標籤下的全部文章。

通過不同的查詢引數組合,就可以得到不同的文章資源列表了。


關注公眾號加入交流群

相關文章