­

第 7 篇:文章詳情的 API 介面

作者:HelloGitHub-追夢人物

一旦我們使用了視圖集,並實現了 HTTP 請求對應的 action 方法(對應規則的說明見 使用視圖集簡化程式碼),將其在路由器中註冊後,django-restframework 自動會自動為我們生成對應的 API 介面。

目前為止,我們只實現了 GET 請求對應的 action——list 方法,因此路由器只為我們生成了一個 API,這個 API 返迴文章資源列表。GET 請求還可以用於獲取單個資源,對應的 action 為 retrieve,因此,只要我們在視圖集中實現 retrieve 方法的邏輯,就可以直接生成獲取單篇文章資源的 API 介面。

貼心的是,django-rest-framework 已經幫我們把 retrieve 的邏輯在 mixins.RetrieveModelMixin 里寫好了,直接混入視圖集即可:

class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    serializer_class = PostListSerializer
    queryset = Post.objects.all()
    permission_classes = [AllowAny]

現在,路由會自動增加一個 /posts/:pk/ 的 URL 模式,其中 pk 為文章的 id。訪問此 API 介面可以獲得指定文章 id 的資源。

實際上,實現各個 action 邏輯的混入類都非常簡單,以 RetrieveModelMixin 為例,我們來看看它的源碼:

class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

retrieve 方法首先調用 get_object 方法獲取需序列化的對象。get_object 方法通常情況下依據以下兩點來篩選出單個資源對象:

  1. get_queryset 方法(或者 queryset 屬性,get_queryset 方法返回的值優先)返回的資源列表對象。
  2. lookup_field 屬性指定的資源篩選欄位(默認為 pk)。django-rest-framework 以該欄位的值從 get_queryset 返回的資源列表中篩選出單個資源對象。lookup_field 欄位的值將從請求的 URL 中捕獲,所以你看到文章介面的 url 模式為 /posts/:pk/,假設將 lookup_field 指定為 title,則 url 模式為 /posts/:title/,此時將根據文章標題獲取單篇文章資源。

文章詳情 Serializer

現在,假設我們要獲取 id 為 1 的文章資源,訪問獲取單篇文章資源的 API 介面 //127.0.0.1:10000/api/posts/1/,得到如下的返回結果:

可以看到很多我們需要在詳情頁中展示的欄位值並沒有返回,比如文章正文(body)。原因是視圖集中指定的文章序列化器為 PostListSerializer,這個序列化器被用於序列化文章列表。因為展示文章列表數據時,有些欄位用不上,所以出於性能考慮,只序列化了部分欄位。

顯然,我們需要給文章詳情寫一個新的序列化器了:

from .models import Category, Post, Tag

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = [
            "id",
            "name",
        ]
        
class PostRetrieveSerializer(serializers.ModelSerializer):
    category = CategorySerializer()
    author = UserSerializer()
    tags = TagSerializer(many=True)

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "body",
            "created_time",
            "modified_time",
            "excerpt",
            "views",
            "category",
            "author",
            "tags",
        ]

詳情序列化器和列表序列化器幾乎一樣,只是在 fields 中指定了更多需要序列化的欄位。

同時注意,為了序列化文章的標籤 tags,我們新增了一個 TagSerializer,由於文章可能有多個標籤,因為 tags 是一個列表,要序列化一個列表資源,需要將序列化器參數 many 的值指定為 True

動態 Serializer

現在新的序列化器寫好了,可是在哪裡指定呢?視圖集中 serializer_class 屬性已經被指定為了 PostListSerializer,那 PostRetrieveSerializer 應該指定在哪呢?

類似於視圖集類的 queryset 屬性和 get_queryset 方法的關係, serializer_class 屬性的值也可以通過 get_serializer_class 方法返回的值覆蓋,因此我們可以根據不同的 action 動作來動態指定對應的序列化器。

那麼如何在視圖集中區分不同的 action 動作呢?視圖集有一個 action 屬性,專門用來記錄當前請求對應的動作。對應關係如下:

HTTP 請求 對應 action 屬性的值
GET list(資源列表)/ retrieve(單個資源)
PUT update
PATCH partial_update
DELETE destory

因此,我們在視圖集中重寫 get_serializer_class 方法,寫入我們自己的邏輯,就可以根據不同請求,分別獲取相應的序列化器了:

class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    # ... 省略其他屬性和方法
    def get_serializer_class():
        if self.action == 'list':
            return PostListSerializer
        elif self.action == 'retrieve':
            return PostRetrieveSerializer
        else:
            return super().get_serializer_class()

後續對於其他動作,可以再加 elif 判斷,不過如果動作變多了,就會有很多的 if 判斷。更好的做好是,給視圖集加一個屬性,用於配置 action 和 serializer_class 的對應關係,通過查表法查找 action 應該使用的序列化器。

class PostDetailViewSet(viewsets.GenericViewSet):
    # ... 省略其他屬性和方法
    serializer_class_table = {
      'list': PostListSerializer,
      'retrieve': PostRetrieveSerializer,
    }
    
    def get_serializer_class():
      	return self.serializer_class_table.get(
            self.action, super().get_serializer_class()
        )

現在,再次訪問單篇文章 API 介面,可以看到返回了更加詳細的部落格文章數據了:


關注公眾號加入交流群