DRF使用超連結API實現真正RESTful

dongfanger發表於2020-12-21

很多API並不是真正的實現了RESTful,而應該叫做RPC (Remote Procedure Call 遠端過程呼叫),Roy Fielding曾經提到了它們的區別,原文如下:

I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.

What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?

— Roy Fielding
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

大概意思是,如果應用狀態引擎(API)不是超文字驅動的,那麼就不是RESTful。

我的理解是,像超文字一樣攜帶一個地址,可以定址定位資訊,如超文字的link屬性。

超連結(Hypermedia)API

Hypermedia指的是,返回結果中提供連結,連向其他API方法,使得使用者不查文件,也知道下一步應該做什麼。比如,當使用者向api.example.com的根目錄發出請求,會得到這樣一個文件:

{"link": {
  "rel":   "collection https://www.example.com/zoos",
  "href":  "https://api.example.com/zoos",
  "title": "List of zoos",
  "type":  "application/vnd.yourformat+json"
}}

上面程式碼表示,文件中有一個link屬性,使用者讀取這個屬性就知道下一步該呼叫什麼API了。rel表示這個API與當前網址的關係(collection關係,並給出該collection的網址),href表示API的路徑,title表示API的標題,type表示返回型別。

摘取自:http://www.ruanyifeng.com/blog/2014/05/restful_api.html

建立api_root的Endpoint

回到教程的例子。在前面我們已經為snippetsusers建立了Endpoint,現在來建立根目錄的Endpoint,編輯snippets/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse


@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users': reverse('user-list', request=request, format=format),
        'snippets': reverse('snippet-list', request=request, format=format)
    })

reverse()函式用來返回snippets/urls.py中viewname對應的url,如path('users/', views.UserList.as_view(), name='user-list')

然後新增到snippets/urls.py中:

path('', views.api_root),

建立SnippetHighlight的Endpoint

還記得在上篇文章中提到的Snippet.highlighted欄位麼:

image-20201220210707722

我們現在為它建立Endpoint,繼續編輯snippets/views.py

from rest_framework import renderers
from rest_framework.response import Response

class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = [renderers.StaticHTMLRenderer]

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

然後新增到snippets/urls.py中:

path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view()),

因為snippet.highlighted不是JSON而是HTML,所以用[renderers.StaticHTMLRenderer]返回預渲染的(pre-rendered)HTML。

HyperlinkedModelSerializer

在Web API設計中,一般有以下幾種方式來表示實體之間的關係:

  • 主鍵
  • 超連結
  • 關係實體(the related entity),唯一識別符號欄位(a unique identifying slug field)
  • 關係實體,預設字串(the default string representation)
  • 關係實體,嵌入到父類中(the parent representation)
  • 其他自定義

前2個比較熟悉,後面幾個有點不太懂,我理解是類似於資料庫的關聯關係表。

DRF支援以上所有方式,這裡我們用DRF的HyperlinkedModelSerializer來實現真正的RESTful。在snippets/serializers.py中把我們之前的程式碼:

class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style', 'owner']


class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']

修改為:

class SnippetSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')

    class Meta:
        model = Snippet
        fields = ['url', 'id', 'highlight', 'owner',
                  'title', 'code', 'linenos', 'language', 'style']


class UserSerializer(serializers.HyperlinkedModelSerializer):
    snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)

    class Meta:
        model = User
        fields = ['url', 'id', 'username', 'snippets']

其中ModelSerializer換成了HyperlinkedModelSerializer,後者的區別如下:

  • 預設不包含id欄位

  • 包含url欄位,用HyperlinkedIdentityField表示

    原始碼:serializer_url_field = HyperlinkedIdentityField

  • 關係用HyperlinkedRelatedField表示,而不是PrimaryKeyRelatedField

    原始碼:serializer_related_field = HyperlinkedRelatedField

由於用了HyperlinkedModelSerializer,SnippetSerializer和UserSerializer的url欄位預設指向的是'{model_name}-detail' url pattern,這是DRF定義的,在示例中就是'snippet-detail''user-detail'。新增的highlight欄位和url欄位是一樣的型別,它指向的是'snippet-highlight',而不是'snippet-detail'

修改url pattern

既然已經提到了url pattern,那麼在snippets/urls.py中修改一下:

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns

from snippets import views

# API endpoints
urlpatterns = format_suffix_patterns([
    path('', views.api_root),
    path('snippets/', views.SnippetList.as_view(), name='snippet-list'),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view(), name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view(), name='snippet-highlight'),
    path('users/', views.UserList.as_view(), name='user-list'),
    path('users/<int:pk>/', views.UserDetail.as_view(), name='user-detail')
])

name就是在serializers.pyviews.py中用到的。

新增分頁

REST設計基本原則提到了:處理好分頁。DRF新增分頁的方式很簡單,編輯tutorial/settings.py檔案:

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

東方說

我之前是在學SpringBoot的時候瞭解過RESTful API的超連結API,文章開頭的那一段介紹就是當時寫的筆記,DRF提供了HyperlinkedModelSerializer來實現,還是比較好理解的,其中的細節需要在實戰中再多多熟悉。

參考資料:

https://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/

https://spring.io/guides/tutorials/rest/

相關文章