【Django】 rest-framework和RestfulAPI的設計


【rest-framework】

  這是一個基於django才能發揮作用的組件,專門用於構造API的。

  說到API,之前在其他項目中我也做過一些小API,不過那些都是玩票性質,結構十分簡單而且要求的設計強度並不高,要求的請求方式也不太規范。要想做一個規范的健壯的API,最好的辦法果然還是站在巨人的肩膀上,學習一些現成組件的使用方法來構建自己的API最為保險簡單。

  下面就來從我的角度簡單說說rest-framework這個框架如何使用。

  本篇主要參考了 官方文檔,以及這個系列的翻譯(第一第二第三、剩余作者還在繼續翻譯中)。

  安裝可以使用pip install djangorestframework

■  何為RESTful的API

  首先有必要了解一下什么是REST,REST的全稱是Representational State Transfer,是一種軟件設計和架構的風格。目的在於提升開發的效率,增加系統的可用性和可維護性。可以把它看成是一種在開發網絡應用過程中產生的最佳實踐。遵循REST風格的接口就是RESTful的API。這種API,考慮到網絡中客戶端和服務端的交互,其本質是CURD(Create,Update,Read,Delete)等各種操作和數據交互,將各種類型的數據交互和具象的HTTP方法聯系起來,形成一套API的設計規則:

  GET /resource  獲取所有resource對象的列表

  GET /resource/ID  獲取指定resource對象

  POST /resource  根據POST數據中的參數新建一個resource對象

  PUT /resource/ID  全量更新指定的一個resource對象

  PATCH /resource/ID  部分更新指定的一個resource對象

  DELETE /resource/ID  刪除一個指定的resource對象

  有了上面這套規范,我們當然可以自己實現一個API了,不過工作量還是偏大一些。rest_framework就是一個很好的遵循了這個規范的框架,我們只需要編寫相對少的代碼就可以得到一個功能健全的API了。

■  rest_framework的基本使用

  rest_framework框架是在django中使用的,為了說明方便,我們假設在一個django項目中我們startapp album這個APP,然后在它的models.py文件中定義了如下一個“專輯”的模型:

class Album(models.Model):
    id = models.AutoField(primary_key=True)
    album_name = models.CharField(max_length=80)
    artist = models.CharField(max_length=100)

    def __unicode__(self):
        return '<Album> %s' % self.album_name

  我們暫時先定義這個簡單的模型,不涉及任何復雜關系。

■   序列化器

  然后我們先來說說序列化器這個東西。在rest_framework中,序列化器是一個位於客戶端和后台之間的中間層。這個中間層一個最基本的作用就是接受前端JSON字符串轉化為后台python可以識別的對象;從后台獲取python對象然后轉化為給前端的JSON格式字符串。當然如果它僅僅是這個作用的話那用json.dumps和json.loads差不多了。一個亮點在於序列化器可以定義一些字段,讓進出的數據可以“一個蘿卜一個坑”地填入序列化器,從而就可以方便地進行格式轉化,順便還可以做做數據校驗這種工作。序列化器的另一個方便之處在於它可以和django的模型層進行互動,從而大幅度減少了編碼量。下面我們來看看具體的序列化器

  序列化器的類在rest_framework.serializers中,最基本的一個序列化器類是Serializer。

  我們通過繼承Serializer類定義自己的序列化器類。通常在類中需要指出本序列化器所有需要進行處理的字段名和字段類型,看起來有點像在定義一個Model或者一個Form。

class AlbumSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    album_name = serializers.CharField(max_length=80)
    artist = serializers.CharField(max_length=100)

  由於序列化器只是一個中間層,用於轉換信息用,所以對於字段的條件校驗可以設置得不那么嚴格。比如serializer.IntergerField()並不能添加primary_key屬性,因為序列化根本不會管主鍵相關的邏輯,是否主鍵沖突或者怎么樣都是最終存儲的時候ORM會幫我們看的。

  接下來我們演示如何使用序列化器進行序列化作業:

# 創建一條測試用的記錄
album = (id=1,album_name='soundtrack',artist='oreimo') 

# 用序列化器序列化對象,獲得序列化后的對象
serialized_album = AlbumSerializer(album)

# 將序列化的對象轉換成JSON格式
from rest_framework.renderers import JSONRenderer
JSONRenderer().render(serialized_album.data)

  序列化器接收單個ORM對象,或者一個QuerySet對象,此時需要加上many=True這樣一個序列化參數。而其返回的serialized_album即所謂的“序列化后的對象”,有很多屬性。其中最重要的是data屬性,它是一個ReturnedDict類型(一種字典的亞種,當進行單個的序列化時)或者一個Orderdict構成的列表(當進行QuerySet的序列化時)。這些內容可以被JSON化,這里沒有直接使用json模塊來JSON化,而是使用了rest_framework自帶的JSONRenderer類來進行JSON化處理。除了data,這類“被序列化后的ORM對象”還有諸如errors,instance,is_valid等屬性和方法,這里暫時都還用不到,后面繼續說。

  除了序列化,序列化器還可以做的就是反序列化:

raw = '{"id":"1","album_name":"soundtrack","artist":"oreimo"}'

from django.utils.six import BytesIO
from rest_framework.parsers import JSONParser

streamed = BytesIO(raw)
jsoned = JSONParser.parse(streamed)

# 獲得到預序列化對象,這個過程一般不出錯
predump = AlbumSerializer(data=jsoned)

# 校驗此對象是否符合要求,比如raw是不是標准的JSON格式,內容中有無不符合定義的字段等
predump.is_valid()

# 如果上句返回了True,那么可以查看被驗證是正確的數據,否則可以查看驗證不通過的具體錯誤信息
predump.validated_data
predump.errors

  其實可以看到,序列化器並沒有參加“反序列化”工作,那些事讓BytesIO和JSONParser做掉了,而這里序列化器所做的,無非是利用其驗證功能,進行了反序列化數據的正確性驗證。只有當驗證通過(is_valid方法返回True),才可以調用validated_data屬性。這個屬性就是Python內部的對象,可以用來進行一些Python內部即web后台的操作了。相反,如果驗證沒通過,那么可以看errors里面的具體錯誤信息。errors也是一個符合JSON規范的對象。is_valid在被調用的時候可以傳入raise_exception=True參數,從而使得驗證失敗時直接拋出錯誤,返回400應答。

  上面驗證過程的is_valid方法是一刀切的,並且驗證規則有限。如果需要自己進行驗證邏輯的指定,那么可以在序列化器類中實現validate方法以進行object層面的數據驗證。此方法接受一個參數data(理想情況應該是一個字典或列表)表示進行validate的時候,整個序列化器中的數據。如果驗證通過,則方法返回值應該是一個合法的和原值類似的結構;若驗證不通過那么需要raise起serializer.ValidationError。此外Serializer類中定義class Meta也可以進行一些object層面的驗證。具體不說了,可以差文檔。

  進一步的如果相對單個單個字段的驗證,並且自己規定驗證邏輯,那么可以在序列化器類中定義名為validate_<field_name>的方法,field_name是你自己定義的字段名,這個方法接受一個參數value,代表進行validate時當前這個字段的值。對於驗證成功與否和object層面的驗證validate方法類似。對於單字段的驗證,還可以通過在Serializer類聲明字段的時候指定validators=[xxx,yyy...],xxx和yyy都是定義好的函數名,

  對於反序列化得到的數據,我們可能希望將其保存成一個ORM對象。此時序列化器對象提供了save方法,比如predump.save()返回的就可以是一個實例。但是目前直接調用會報錯,因為save方法的調用前提條件是要實現類中的create和update方法:

    def create(self, validated_data):
        return Student(**validated_data)

    def update(self, instance, validated_data):
        instance.id = validated_data.get('id',instance.id)
        instance.name = validated_data.get('name',instance.name)
        instance.phone = validated_data.get('phone',instance.phone)
        instance.comment = validated_data.get('comment',instance.comment)
        instance.save()
        return instance

  這是在StudentSerailizer類中實現的兩個方法,create方法很簡單,就是講傳入的字典分散入參,創建一個對象返回。update方法略復雜一些,要把所有屬性都安裝上面的樣子寫出來。這主要是考慮到validated_data不全量的情況。即update時可能validated_data並不是全量信息,可能只提供了部分字段。,此時就要保證其余字段不被這次update所影響。那么create方法和update方法(全量和部分)都是在什么情況下被調用的呢?

  首先這兩個方法都是serializer對象調用save方法時使用。而這個serializer對象得來時,可能通過這幾種方法得來:

  StudentSerializer(data=data)  data是一個合法的Python結構比如一個字典,此時要求其實全字段的字典。這樣得到的serializer對象調用save時相當於執行create方法。

  StudentSerializer(student,data=data)  還可以這樣得到一個serializer對象。student是一個既存的Student類的ORM對象。此時調用update方法。傳入的data字典,如果是全字段字典,那么自然就是全字段更新,否則就是部分字段更新。

  需要注意的是serializer對象的save方法可不是ORM對象的save方法,它的save只是將字典中的數據充入到一個ORM對象中,而后者的save則是將ORM中的數據落地到數據庫中。

 

■   ModelSerializer,更加方便的序列化器

  (這部分內容更多的擴展寫在了下面包含關系的序列化器一節中)

  繼承Serializer雖然可以實現一個序列化器,但是編碼量仍然比較多,因為還是要一個個手工輸入字段以及字段相關性質。更方便的序列化器是ModelSerializer。這個就是之前說到的可以和django模型層進行互動的序列化器。比如針對上面這個模型我們可以設計這么一個序列化器:

#-*- coding:utf-8 -*-

from rest_framework import serializers
from album.models import Album

class AlbumSerializer(serializers.ModelSerializer):
    class Meta:
        model = Album
        fields = ('album_name','artist')

  通常序列化器是寫在項目主目錄下的serializers.py文件中。可以看到,通過在序列化器類中定義Meta子類,可以指定這個序列化器和哪個模型關聯,並且指出引用模型類的哪些字段。如果fields參數寫'__all__'的話那么就是引用所有字段了。

  如果想要除外某些字段可以設置exclude屬性比如exclue = ('a',),類似的,如果想要設置某些字段的只讀特征(當視圖update這些字段的時候會被拒絕,create的時候也不需要提供這些字段)

■    視圖設置

  在寫完序列化器之后再看后台的 路由設置 和 視圖設置。首先來說說視圖設置。在rest_framework框架下的API視圖有兩種實現方式。一種是基於函數的,另一種基於類。基於函數的實現方式用到了rest_framework.decorators中的api_view裝飾器。通過為一個函數加上 類似於 @api_view(['GET','POST'])的裝飾器可以將這個函數作為API的視圖函數。參數是和flask框架類似的,指出了該函數支持的方法類型。另外下面的函數應該帶有request這個參數。基於函數的視圖設置方法雖然和django原生的很像,不過有些僵硬的是它把本來應該有機一體的函數分成了好幾塊,不太好。

  另一種基於類的視圖是我重點想說的。這個類也可以寫在views.py中,其繼承自rest_framework.views.APIView這個類,在類中,應該要實現get,post等方法,並且給出相應的回應。比如像這樣的一個類:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from rest_framework.views import APIView
from rest_framework.response import Response
from albumtest.serializers import AlbumSerializer
from rest_framework import status
from models import Album
# Create your views here.

class AlbumList(APIView):
    def get(self,request,format=None):
        serializer = AlbumSerializer(Album.objects.all(),many=True)
        data = serializer.data
        return Response(data,status=status.HTTP_200_OK)

    def post(self,request,format=None):
        serializer = AlbumSerializer(request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data,status=status.HTTP_200_OK)
        return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)

  基於RESTful的思想,其實對於一個模型,我們可以將各個操作涇渭分明地分別寫在兩個類里,分別是AlbumList(就是上面這個)和AlbumDetail。前者用於訪問/resource的獲取全對象和新增對象兩個操作;后者用於訪問/resource/ID的查改刪單個對象的操作。更加詳細的類在下面給出,我們先進入get方法看下具體代碼。get方法接受了一個request參數和format參數。實例化AlbumSerializer這個序列化器用到的是一個名為instance的參數,可以接受單個模型實例,也可以接受一個queryset,當參數是一個集合的時候應該加上many=True參數來保證有效的序列化。

  實例化之后的序列化器有很多屬性,最常用的就是data屬性,表示該序列化器對象序列化出的內容(字符串)是什么,然后通過rest_framework框架提供的Response類返回結果字符串。另外status參數也是必要的,rest_framework也提供了status這個類,其中有一些常量供我們使用。

  再來看post方法,post方法是從客戶端接受數據解析后傳給后台的,這樣子的話就用到了request參數中的data屬性。注意這個request參數有別於一般django視圖函數中的request參數,總之我們調用其data屬性就可以獲得這個POST請求的參數了。之后是is_valid方法,這個方法是serializer自帶的一個方法,可以根據定義時字段的控制來校驗得到的參數格式等是否正確。如果不正確,無法合法落庫,自然需要返回錯誤信息。此時調用了序列化器的另一個屬性errors,它包含的就是序列化過程中發生的錯誤信息了。

  ●  路由設置

  rest_framework也為我們提供了方便的路由設置的辦法(通過rest_framework.routers),由於路由設置即便是手寫也還好不過幾行代碼而已,所以這里暫時用手動設置的辦法來設置路由。

  這次我們將路由設置在主目錄下的urls.py中,這里已經預設了/admin的路由。我們加上兩條:

from django.conf.urls import url,include
from django.contrib import admin
from album import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^albums/',views.AlbumList.as_view()),
    url(r'api-auth',include('rest_framework.urls'))
]

 

  可以看到,之前定義的視圖類,我們通過調用as_views()方法來使其轉換成可以被django的url方法接受的參數。前面配置的路徑是/albums,也就是說對於Album這個模型,我們基於RESTful思想的API設計中的那個resource就是albums了。也就是說GET訪問/albums可以獲取所有albums的列表這樣子。

  第二條是一個rest_framework對於API驗證的支持。加上這項配置之后,我們可以對訪問API的當前用戶(匿名或者普通或者超級用戶等等)做出感知並且給出相關的回應。這方面內容在后面細講。

  為了讓以上一大票rest_framework框架相關的內容生效,也別完了在settings.py的INSTALLED_APPS中加上'rest_framework'。

  

  OK,至此這個API已經可以正常運行了,別忘了同步數據庫信息等django通用步驟。然后我們通過瀏覽器訪問目標地址的/albums路徑可以看到這樣一個界面;

  看到的是個界面而不是一個JSON串,這是因為通過瀏覽器訪問時,在請求頭中的Accept字段默認是html(這類似於javascript發起的ajax請求中的dataType字段寫什么),如果一個請求的Accept指明是json或者是默認值的話,那么就會返回json串了。

  順便一提,這個界面是rest_framework為了方便開發和調試加入進來的,確實很好用。但是在生產上這個界面肯定是要關掉的。具體怎么關我還要研究下。。。

  ●  關於AlbumDetail類

  剛才提到了AlbumList類是針對訪問/album的,那針對/album/<ID>的那個類給出如下:

class AlbumDetail(APIView):
    def get_object(self,pk):
        try:
            return Album.objects.get(id__exact=pk)
        except Album.DoesNotExist,e:
            raise Http404

    def get(self,request,pk,format=None):
        album = self.get_object(pk)
        serializer = AlbumSerializer(album)
        return Response(serializer.data)

    def put(self,request,pk,format=None):
        album = self.get_object(pk)
        serializer = AlbumSerializer(album,data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data,status=status.HTTP_200_OK)
        return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)

    def delete(self,request,pk,format=None):
        album = self.get_object(pk)
        album.delete()  # 這個delete方法就是django模型對象自帶的那個,和rest_framework框架無關
        return Response(status=status.HTTP_204_NO_CONTENT)

 

  需要注意幾個點,get_object方法不是rest_framework要求的,但是有了這個方法的話可以讓整體的耦合度升高。除了這個方法之外其余所有方法都還有參數pk,其意思是primary_key,即這個模型對象的主鍵,也是rest_framework從請求url中解析出來的那個標識符。一般情況下是個unicode的字符串。在get_object中,之前的范文中寫的是get(pk),這會報錯,穩妥一點還是加上id__exact=pk吧。

  其他所有方法也都是用到了get_object,另外的到沒什么好說的了,反正都是類似的。

  然后記得在urls.py中的urlpatterns中加上這條:

url(r'^album/(?P<pk>[0-9]+)/$', views.AlbumDetail.as_view())

  需注意,之前如果r'^album/'最后沒加$,要加上,否則/album/1請求有可能會路由到那邊去。然后重啟應用就可以訪問了。可以嘗試用PUT方法改變一個Album對象或者用DELETE方法刪除一個對象。

  關於正則url規則中pk的命名:一般而言這個變量名是可以自己控制的,只要將相關視圖方法中的參數也改名即可。但是在繼承封裝度比較高的視圖類時,最好不要改,因為默認是識別pk這個變量名的。

 ■  請求與響應

  在上面的基本使用的說明之后,我們再來看下rest_framework這個框架的各個細節部分。首先是在這個框架下的請求與響應。

  在基於類的視圖中,我們定義了各個API方法,在方法的參數中帶有request參數作為一個請求的映射。實際上這個request是一個繼承自HttpRequest的東西,它更加適應RESTful風格的API。其核心要素時request.data屬性,它代表了一般視圖中request.POST,requset.PUT,request.PATCH三種可能的數據結構。

  在API方法中我們最終都回復了一個Response對象(即便是DELETE方法這種無需回復信息的請求我們也回復了204),

  另外在上面給出的,基於類的視圖中,其實已經可以感覺到,通用性是蠻大的。把Album換成另外一個模型,似乎這些代碼也不用怎么改就能直接用。這就說明代碼還有更精簡的空間。實際上,這種分成List和Detail兩個類的模式rest_framework確實也給出了這種模式方便的實現方式。名曰泛類實現(generic class):

from rest_framework import generics
from serializers import AlbumSerializer
from models import Album

class AlbumList(generics.ListCreateAPIView):
    queryset = Album.objects.all()
    serializer_class = AlbumSerializer

class AlbumDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Album.objects.all()
    serializer_class = AlbumSerializer

  這里使用泛類實現的基於類的視圖,和上面自己一個個寫API方法,在使用上是一模一樣的。不過泛類大大減小了編碼量。

■  包含關系的序列化器

  剛才我們給出的模型十分簡單和單一,沒有涉及到關系。自然,就會想當涉及到關系時,rest_framework是如何處理關系的。為了說明關系,我們改造了一下現有的Album模型並新增了Track模型:

class Album(models.Model):
    id = models.AutoField(primary_key=True)
    album_name = models.CharField(max_length=80)
    artist = models.CharField(max_length=100)

    def __unicode__(self):
        return '<Album> %s' % self.album_name

class Track(models.Model):
    album = models.ForeignKey(Album,on_delete=models.CASCADE,related_name='tracks')
    order = models.IntegerField()
    title = models.CharField(max_length=100)
    duration = models.IntegerField()

    class Meta:
        unique_together = ('album','order')
        ordering = ['album']

    def __unicode__(self):
        return '<Track %s> %s' % (self.order,self.title)

 

  可以看到Track中有album字段,是個Album模型的外鍵連接。並且設置了反向引用時的引用名是tracks(重要!)

  在這種時候,理想情況下,我們GET /albums這個API時,返回的結果中最好有一個tracks字段,其包含了一些和這個album對象相關的track的信息。要達到這種效果,最關鍵的是要改造序列化器。

  rest_framework中有一個relations.py文件,里面包含了我們可能在序列化器中用到的很多關於關系操作的字段。比如我們將AlbumSerializer這個序列化器改造成這樣子:

class AlbumSerializer(serializers.ModelSerialzier):
    tracks = serailizers.StringRelatedField(many=True)
    class Meta:
        model = Album
        fields = ('album_name','artist','tracks')

 

  雖然繼承了ModelSerializer,但是還是重寫了tracks這個字段,將其指定為一個StringRelatedField,通過這樣的改造,得到的某一個album的tracks字段就是一個字符串的列表,每一個成員都是track模型調用__unicode__方法返回的結果。

  當然除了StringRelatedField之外,還有很多其他的,比如

  PrimaryKeyRelatedField  得出的列表是相關模型的id組成的列表

  HyperlinkedRelatedField  返回的列表是相關模型詳細信息的url的列表,需要指定view_name,這個view_name應該和urls.py中/resource/ID這個路由指出的name一致,這樣才可生成正確的url列表

  SlugRelatedField  上述返回的列表內容的字段都是這個Field指定好的(主鍵或者相應的url等),而這個SlugRelatedField可以讓我們自己指定,只要在參數中寫上slug_field='字段名'即可。如SlugRelatedField(many=True,read_only=True,slug_field='title')時,返回的tracks的列表中就是各個tracks的title字段的值了。

  類似功能的Field也還有一些,值得注意的是,除了最上面的StringRelatedField,其他所有Field都要加上queryset參數或置參數read_only=True。這主要是因為當我們為API添加關系之后,如果我們要通過一個模型的API去修改另一個模型的值時,rest_framework框架需要queryset去校驗數據的合法性。比如這里我們可以queryset=Track.objects.all(),不過這么搞不是很符合RESTful思想(修改一個模型應該在這個模型自己的API中進行),所以一般設置成read_only會比較好。但是如果涉及到新增,那么就必須設置一個queryset了,否則將無法POST爭取的數據用於新增。

  ●  返回完整關系--序列化器之間互相調用

  上面不論是用哪個Field,返回的tracks字段的列表中都是相關track的部分信息,有沒有辦法返回完整的信息呢?答案是不用serializers提供的Field,而是直接使用自己的Serializer來實現。

  首先改造序列化器:

from rest_framework import serializers
from album.models import Album,Track

class TrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = Track
        fields = ('order','title','duration')

class AlbumSerializer(serializers.ModelSerializer):
    # tracks = serializers.StringRelatedField(many=True)
    # tracks = serializers.PrimaryKeyRelatedField(many=True,queryset=Track.objects.all())   這些是上面提到的Field,順便做個示例,就不刪了
    # tracks = serializers.SlugRelatedField(many=True,read_only=True,slug_field='title')
    tracks = TrackSerializer(many=True,read_only=True)
    class Meta:
        model = Album
        fields = ('album_name','artist','tracks')

  tracks字段直接被定義成了目標模型的序列化器,由於類之間的引用關系的規定,這導致TrackSerializer必須定義在AlbumSerializer之前。這樣子再去調用時,tracks字段就是一個字典的列表了,每一個字典帶的就是每個track的order,title,duration字段的信息。

  ●  可寫的完整關系

  通常默認情況下,上面這種直接調用別的Serializer作為一個RelatedField而形成的關系是只讀的。比如上面例子中,我們可以GET到某個Album中的一個Track對象的列表,但是在POST時不允許你也帶這么一個Track對象的列表,后台不會識別並且將這些tracks作為新紀錄增加進數據庫。要實現可寫的完整關系,需要在AlbumSerializer這樣一個序列化器類中重載create方法比如:

class TrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = Track
        fields = ('order', 'title', 'duration')

class AlbumSerializer(serializers.ModelSerializer):
    tracks = TrackSerializer(many=True)

    class Meta:
        model = Album
        fields = ('album_name', 'artist', 'tracks')

    def create(self, validated_data):
        tracks_data = validated_data.pop('tracks')
        album = Album.objects.create(**validated_data)
        for track_data in tracks_data:
            Track.objects.create(album=album, **track_data)
        return album

 

  ●  自定義RelatedField

  雖然上面說了三四種RelatedField基本上可以滿足很多場景了。但是總還是有些場景無法滿足,比如我想這個RelatedField形成的列表是目標對象類的其中的某幾個(如果是一個的話用SlugRelatedField即可)字段的值以一定格式形成的字符串,此時就需要我們自己來實現一個RelatedField。常見的,我們在目錄中創建一個fields.py文件,然后在里面寫:

class TrackFormatField(serializers.RelatedField):
    def to_presentation(self, value):
        return 'Track %s: %s' % (value.order,value.title)

  看到我們重載了to_presentation方法,這個方法控制的就是序列化器是以何種格式序列化輸出信息的,它接受一個value參數,指的是被傳入這個字段的那個對象,返回一個字符串。然后在AlbumSerializer里面就可以令

  tracks = TrackFormatField(many=True),這樣就使得最終出來JSON串中,tracks字段值的列表中每項的格式都是Track 1: xxx這樣子。

  ●  通過重載serializers.Field自定義Field

  上面講的自定義RelatedField是針對一些有表外關聯的字段序列化時可以用的辦法。如果對於有些字段單純只是想做一個翻譯或者其他不涉及其他模型的處理,那么可以考慮重載Field這個更加底層的類。

  繼承Field類時,必須要進行重載的方法有to_presentation以及to_internal_value。兩者分別是用來將Python對象轉化為字符串以及字符串轉化為Python對象的。下面給出一個例子

class EnvAdaptField(serializers.Field):
    def __init__(self,envDict,*args,**kwargs):
         self.envDict = {k:v for k,v in envDict}
         serializer.Field.__init__(self,*args,**kwargs)
  
    def to_presentation(self, value):
        return self.envDict.get(value,'未知');

    def to_internal_value(self, data):
        return data

env = [('0','生產'),('1','SIT'),('2','UAT')]
# 在某個serializer中
environment = EnvAdaptField(env)

 

  首先不必這么死板,除了to_presentation和to_internal_value之外還可以重載一些其他方法。注意有時不是完全重寫的話記得做一些類似於super的操作。比如這里重載了__init__方法之后使得自定義Field還可以接收一些額外的數據。從下面的env中不難能看出來,這個Field的目的在於將所屬環境這樣一個屬性,用0,1,2等標識存儲在數據庫中,但是客戶端獲取數據時給的數據又要是生產,SIT等具體的文字描述。

  to_presentation方法做到了把標識翻譯成具體的文字。而to_internal_value的時候我們可以在前端做一些工作,比如select中的<option value="0">生產</option>,這樣可以做到val()提交的時候本來提交的就是標識了,所以不做任何處理直接return 出來即可。

  另外還有一些時候,可能需要在接受到一個請求之后,基於請求指向的那個對象做一些邏輯判斷,對於邏輯不過關的請求做出拒絕回復。這應該怎么做?如果這個判斷要放在field這個層面的話,那么在Field類中我們還可以重載另外一些方法比如get_attribute和get_value。這兩個方法的作用分別是作為了to_presentation和to_internal_value的前處理方法。也就是說兩個方法的返回分別是to_presentation和to_internal_value的參數。

  對於上面的這個需求,我們可以這么干:

class empty:
    pass

class AccountInUseField(serializer.Field):
    def get_value(self, dictionary):
        # 校驗放在這個方法里做
        for_sever = dictionary.get('for_server')
        request = self.context.get('request')
        tobe_in_use = dictionary.get(self.field_name, empty)
        if request.method in ('POST','GET','DELETE'):
            return tobe_in_use
        if [row[0] for row in Server.objects.get(id=for_server).accounts.values_list('in_use')].count(True) > 0 and tobe_in_use:
            # 一個服務器的賬號最多只能有一個的in_use字段處於True狀態
            raise serailizer.ValidationError({'detail': 'In use account spotted'})
        return tobe_in_use

    def to_internal_value(self,data):
        return data

  這個類沒寫完整,在我的實踐中(DjangoORM+restframework構造的RESTful標准API),通過API發出針對賬號模型的請求時,就會通過這個類的代碼。首先可以看到的是get_value方法接收了一個dictionary參數。其內容實際上是發來的所有請求參數的鍵值對。比如PUT請求就會把指向的這個模型的所有字段都給寫出來,相對的PATCH請求可能就只會有一部分字段。self.field_name並不是由Field決定,而是在模型的serializer中通過調用Field類的Bind方法來決定的。比如在這個示例中self.field_name應該等於'in_use'。不寫死成'in_use'也是考慮到了和源代碼的一個適配性。同理,如果發來請求中沒有in_use字段的話,dictionary中get也只能獲得None,但是None對於一些輸入輸出標准來說是錯誤,源代碼(rest_framework/fields.py文件中)定義了空類empty來代表空值而非None,這里也是模仿他。

  再來看具體代碼,self.context是Field類工作的上下文,是一個字典包含了view和request兩個鍵。view的值自然就是請求由哪個視圖處理,按照rest_framework標准,這個視圖會關聯一個serializer,而serializer中Bind了我們這個Field。request嘛自然就是這個請求本身了,獲取到request之后我們就可以像在視圖函數里獲取request對象那樣在這里也做類似的操作。比如通過request.method對請求方法做了一個過濾。如果是POST,GET,DELETE等方法,沒必要做校驗就直接返回掉。而PUT,PATCH等方法,由於是改數據,很可能會觸及一個服務器賬號只能有一個in_use是True的限制,所以要校驗。校驗條件涉及到我具體的模型設計,不多說了。總之當條件未通過時,我們raise了一個ValidationError,此時API就會知道本次請求失敗了,從而返回失敗的HTTP Response。默認情況下status_code是400。另外需注意ValidationError中的參數是一個字典不是一個簡單字符串(雖然官方文檔明明說簡單字符串就行了。。)。另一種拋出錯誤的辦法是self.fail方法。fail方法接受固定的幾種錯誤模式作為Key比如fail('incorrect_type'),fail('incorrect_format')等,並給出了默認的信息,如果有需要也可以用。

  最終如果通過了校驗,沒有raise起錯誤,那么就返回要修改的in_use值,給to_internal_value方法處理即可。

 

■  API的權限控制

  通常API是需要有權限控制的。rest_framework給我們提供了大部分常見的權限模型,可以立即使用。但是說到權限控制,也是分成好多維度的。首先應該明確,控制的直接對象都是各種方法,即允許不允許某一個方法的執行。而控制的層級可以分成模型層面的和實例層面的。首先來說說前者。

  ●  模型層面的控制

  模型層面的控制,只需要我們在視圖類中直接添加即可。比如我們想對於Album這個模型做權限控制,將Album的兩個視圖類改寫如下:

from rest_framework import permissions

class AlbumList(generics.ListCreateAPIView):
    queryset = Album.objects.all()
    serializer_class = AlbumSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

class AlbumDetail(generics.RetrieveUpdateDestroyAPIView)
    queryset = Album.objects.all()
    serializer_class = AlbumSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

  rest_framework框架自帶的permissions類預定義了很多權限管控的模型,比如我們用到的IsAuthenticatedOrReadOnly就是指出了對於這個視圖類包含的操作,要么只能讀,涉及增刪改操作的只能在當前用戶通過驗證的情況下進行。

  ●  對象層面的控制

  對象層面的權限控制則稍微復雜一些。我們需要重新定義權限類的樣子。我們可以在app中新建一個permissions.py文件。然后在這個permissions中自定義權限類。至於這個類怎么定義回頭再說。首先我們要明白,對象層面上的權限控制,勢必要求將我們編寫的內容和django自帶的用戶模型(當然自己定義的用戶模型的話就沒必要了)關聯起來。所以需要對Album模型做以下改動:

class Album(models.Model):
    id = models.AutoField(primary_key=True)
    album_name = models.CharField(max_length=80)
    artist = models.CharField(max_length=100)
    owner = models.ForeignKey('auth.User',related_name='albums',on_delete=models.CASCADE)

    def __unicode__(self):
        return '<Album> %s' % self.album_name

  通過一個外鍵,將django的用戶和本模型聯系起來。順便,可以不特地from django.contrib.auth.models import User而直接使用字符串形式的'auth.User'。然后為了方便,我們干脆把整個數據庫清空,重新錄入數據。這里需要額外提一句,如果從admin界面進行Album新增的話,自然是可以選擇owner是誰。但是如果通過API的PUT方法來新增,API目前還無法識別出owner是和當前用戶關聯在一起的。為了讓API能夠自動將發送請求的當前用戶作為owner字段的值保存下來我們還需要做的一件事就是重載視圖類中的perform_create方法。如:

class AlbumList(generics.ListCreateAPIView):
   ...
   def perform_create(self, serializer):
       serializer.save(owner=self.request.user)

   注意在視圖類中自帶了屬性self.request用於對請求中信息的提取(詳情見前面我們從APIView開始繼承時每個方法中有request參數這一點),perform_create大概就是這個類用於保存實例的方法。

  好了,定義好了視圖之后,再回頭去編寫我們需要的權限類。比如一個權限類可以這樣寫:

from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.owner == request.user

 

  自定義的權限類繼承自rest_framework內建的權限類permissions.BasePermission,在這個類中我們重載了has_object_permission方法,重載這個方法說明我們要做的是對象層面的權限控制。這個方法自帶了request,view,obj三個參數,比較多的參數保證了我們可以實現非常靈活的對象層面權限控制。permissions.SAFE_METHODS其實就是('GET','HEAD','OPTION'),當請求的方法是這三個其中的一個的時候,性質都是只讀的,不會對后台數據做出改變,所以可以放開,因此直接return True。當請求其他方法的時候,只有當相關對象的owner字段和當前請求的用戶一致的時候才開放權限,否則拒絕。

  我們還可以將SAFE_METHODS那條判斷去掉,這樣的話就是所有對非本人創建的,單個對象的訪問都會被拒絕。

  順便一提,被拒絕時給出的json回復是{"detail": "Authentication credentials were not provided."}(沒有登錄時)或者{"detail": "You do not have permission to perform this action."}(登錄用戶非owner時)。

  最后一步,就是如何把這個自定義的權限類和實際的視圖類結合起來了,其實和之前rest_framework自帶的IsAuthenticatedOrReadOnly一樣,將這個類賦予視圖類的permission_classes屬性中即可。

 

■  API的用戶驗證

  與權限控制相輔相成的是驗證工作。上面的權限控制的重要一個維度就是當前是否有有效用戶登錄,而這個就取決於用戶驗證工作了。從概念上來說,驗證和權限控制應該分開來看,但是兩者常常在一起用。

  和權限體現在視圖類的permissions_classes屬性中類似的,驗證體現在視圖類的authentication_classes中,表明這個視圖類涵蓋的方法只認可這幾種方式的驗證,通過其他方式的驗證的用戶都視作沒有驗證;假如此時權限上對沒有驗證的用戶有訪問控制的話自然就訪問不到了。

  下面來看一下rest_framework自帶的一些驗證類

  ●  TokenAuthentication

  通過Token驗證時B/S架構中常見的一種驗證方式。其要義是用戶先通過有效的賬號和密碼獲取到一個token字符串,之后所有的請求只需要通過這個token作為一個標識,服務器認可token串就開放相關權限供客戶端操作。rest_framework自帶了一個rest_framework.autotoken的應用。我們可以通過它來較為簡單地建立起一個token體系。

  首先在settings.py的INSTALLED_APPS中假如rest_framework.autotoken應用。然后注意,這個應用定義了幾個token相關的模型,所以要python manage.py migrate及時同步數據庫。 

  接下來就是要構建一個帶有token獲取功能的url,這個url極其相關視圖函數我們可以手動寫,但rest_framework給我們提供了一些預設好的東西。比如:

from rest_framework.authtoken import views as token_views

# 在url中加入:
    url(r'^api-token-auth/',token_views.obtain_auth_token)

 

   如此,通過POST方法訪問api-token-auth路徑,並且帶上POST參數{username:'xxx', password: 'xxx'},如果賬號密碼正確,就會返回一個token字符串了。

  那么得到了這個Token之后怎么樣才能把它用於實際請求中呢?只要在頭信息中加上兩條額外的記錄,比如通過python的requests模塊發起請求的話就像下面這樣:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import requests
headers = {
    'WWW-Authenticate': 'Token',
    'Authorization': 'Token 506c12a0fc447918957ab7198edb7adae7f95f84'
}
res = requests.get('http://localhost:8000/albums/',headers=headers)
print res.status_code
print res.content

 

  需要注意的是,Authorization字段的值不僅僅是Token字符串,還包括了'Token '(最后帶一個空格),當頭信息中帶有這樣的記錄,再訪問authentication_classes中有TokenAuthentication的視圖類時,就可以正常得到數據了。

  當一個視圖類中只有TokenAuthentication時,rest_framework提供給我們調試用的頁面是無法正常工作的,因為那個頁面上無論請求html或json,其請求的頭信息中都不會有Authrorization這個字段。即使有值也不是Token xxx形式。所以為了這個頁面正常工作(同時有時候會需要向網站內部用戶提供訪問,只要當前訪問用戶是有效用戶就可以在頁面上做一些增刪查改操作,而這個操作就可以是調用API的動作),我們應該在有TokenAuthentication類的視圖類中也加上SessionAuthentication這個類,確保比較友好的調用特性。

  ●  SessionAuthentication

  這種驗證法基於django自帶的會話機制。比較好理解,即一個會話只要通過了驗證,那么這個會話就可以發起一些帶有權限控制操作的請求了。

  另外由於django層面上的要求,當發起PUT,POST,DELETE等方法的請求時會要求請求帶有csrf_token字段的信息。然而這個csrftoken在這里就是一個比較tricky的東西了。

  是這樣的:當我們指出多種認證機制的時候(比如SessionAuthentication和TokenAuthentication共用時),我們知道SessionAuthentication是要求服務端發出一個token給客戶端作為應用的,然而TokenAuthentication的場合並不需要,所以此時在發起請求時必須指出:“我的請求是要通過SessionAuthentication類來驗證的”(反過來,當我們的請求要通過TokenAuthentication來驗證的時候也要在請求中指出,這個在上面提到了),否則服務端將不獲取請求中的token信息,這就會導致在發起不安全請求如POST,PUT,PATCH等的時候報錯:{"detail":"CSRF token not found"}之類的錯誤。

  解決辦法:和TokenAuthentication類似,我們在請求的header中添加一個字段的信息:X-CSRFTOKEN即可。不過請注意,這個字段的值並不是我們為了泛泛解決django中POST之類請求需要csrftoken時,添加的ajax全局參數csrfmiddlewaretoken,而應該是一個保存在本地cookie中的一個csrftoken(因為Session的本質就是通過cookie實現的)。這也是我對比了API調試界面發起的請求和在自己開發的頁面上發起請求不同得出的結果。具體來說,我們只要在發起的ajax請求中添加如下:

function getCookie(key){
  // 這個函數是為了方便取cookie值而寫的
  var cookie = document.cookie;
  var kvs = cookie.split(';');
  var i = 0;
  while (i < kvs.length){
    var kv = kvs[i].split('=');
    if (kv[0] == key){
      return kv[1];
    }
    i++;
  }
  return null;
}

$.ajax({
//發起ajax請求,其他參數省略
  headers: {
    'X-CSRFTOKEN': getCookie('csrftoken')
    //因為有'-'在里面,所以X-CSRFTOKEN必須有引號引起來
  }
})

  這樣就不會報錯啦!

  ●  自定義驗證類

  和自定義權限類(上面實現對象層面的權限控制時提到的)一樣,驗證類也可以自己定義。這個類需要繼承自BaseAuthentication

  然后這個類要實現authenticate方法。在這個方法中實現驗證的邏輯,當用戶沒有給出驗證信息時應該返回None,當用戶給出錯誤的驗證信息時拋出AuthenticationFailed錯誤,當用戶驗證成功時返回一個元組:(user,auth),user是通過驗證的用戶對象,auth則是驗證用的關鍵信息。比如可以參考下TokenAuthentication這個類的實現,看下源碼就會明白了。

  除此之外,我們還可以重載authenticate_header方法,這個方法接受request作為參數,並且返回一個字符串。當用戶沒有提供有效的驗證信息,服務端返回401錯誤時,這個字符串會被包含在回應的頭信息的WWW-authenticate字段中,提示用戶提供何種驗證信息。

  下面來具體實現一個類,這個類規定的驗證方式很簡單粗暴,就是要求請求頭信息中帶有用戶名,只要用戶名存在在我們的系統中就算通過認證。這樣的:

from rest_framework import authentication
from rest_framework import exceptions
from django.contrib.auth.models import User

class UsernameAuthentication(authentication.BaseAuthentication):
    def authenticate(self,request):
        username = request.META.get('X-USERNAME')
        if not username:
            return None
        try:
            user = User.objects.get(username__exact=username)
        except User.DoesNotExist:
             return exceptions.AuthenticationFailed('No Such User')
        return user,username

 

  哦對了,這部分代碼可以類比權限類,重新建一個authentications.py文件來存放。

  包含這個權限類的視圖類,訪問時需要在頭信息中加入X-USERNAME的信息。順便一提,用python的requests庫加入頭信息時會發生一個神奇的事情,我設置的明明是X_USERNAME,但是實際發出的頭信息中字段名卻變成了HTTP_X_USERNAME...,需要注意。

 

 ■  過濾

  至此,我們的API已經有了較為完備的功能,但是實際使用還是不行的,因為它只能傻傻地返回一個模型的所有記錄or指定的單個記錄。實際上,API通常會允許請求中包含一些所謂的過濾參數,來把整個結果集進行縮圈、排序,從而得到一個更加漂亮干凈的數據集合。

  rest_framework框架整合了django-filters這個django組件,通過它可以很方便地實現具有足夠用過濾功能的API。

  首先要保證django-filters已經正確安裝了,pip install django-filter(注意沒有s,否則會裝上另一個模塊的)即可。另外在settings.py的INSTALLED_APPS中也要包含上django-filters。(這步不要忘。。否則會在后續的訪問過程中報找不到django-filters相關的模板)。另外,通過django-filters和rest_framework合作做出來的過濾器都是通過GET方法加上一定的參數請求特定的API的url,來獲取一個有限的結果集的。

  接着找到我們要進行過濾的一個視圖類,這個類通常是那種返回整個結果集的,也就是說是ListAPIView繼承出來的那種視圖類。如果想要給Detail的視圖類增加一個過濾器,不是不可以,但是意義不大(畢竟url已經幫你縮圈到只有一個對象了),而Detail類視圖過濾器的條件若是和當前對象不符合的話就會爆出404錯誤。綜上,接下來的所有說明都基於一個完整結果集的那種視圖類,比如AlbumList這個類,我們改成這樣:

# 在views.py文件中 #
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentications import SessionAuthentication
from models import Album
from serializers import AlbumSerializer

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter

class AlbumList(generics.ListCreateAPIView):
    queryset = Album.objects.all()
    serializer_class = AlbumSerializer
    permission_classes = (IsAuthenticated,)
    authentication_classes = (SessionAuthentication,)
    #下面幾行是新的
    filter_backends = (DjangoFilterBackend,OrderingFilter)
    filter_class = AlbumFilter
    ordering_fields = ('artist',)

 

  首先在視圖類中我們引入了filter_backends屬性,指定這個屬性的值中有DjangoFilterBackend,相當於是把rest_framework和django-filters兩個組件聯系了起來。OrderingFilter來自rest_framework.filters而非django-filters下面的某個包(切記切記,為了找這個浪費好多時間。。),把它也加入filter_backends是為了讓這個視圖類可以支持排序。說到排序,ordering_fields指出了這個API的排序可以指定哪些字段作為排序基准。調用時加入ordering參數可以拿來排序,像這樣:ordering=-artist(以artist為字段desc排序),ordering=artist,album_name(以artist和album_name兩個字段asc排序)。

  另外,沒有在上面的代碼中寫出來的是,可以添加一個ordering屬性來指定默認的排序基准,比如ordering = ('artist',) 就是指默認情況下以artist字段做升序排序,不帶任何參數請求時返回結果的排序就是這樣的。

  然后再來看重頭戲,AlbumFilter是一個Filter類,是我們自己實現的,它指出了這個API裝的過濾器支持哪些字段的什么樣的過濾。AlbumFilter這個類我們放在了同目錄下的filters.py文件中,這么寫:

from django_filters import FilterSet
from models import Album

class AlbumFilter(FilterSet):
    class Meta:
        model = Album
        fields = {
            'album_name': ['exact','contains'],
            'artist': ['exact','contains'],
            'tracks__title': ['exact'],
        }

   還是很好懂的,Meta中的model指出了這個過濾器是給哪個模型用的,fields指出了該模型各個字段都支持怎么樣的過濾方法,遵循djangoORM的規則。字段應該和給出的模型對應,如果有該模型沒有的字段則會報錯。另外強大的是可以做關系查詢,即tracks__title表明在API的過濾器中我們可以進行關系的查詢了! 各個字段的值是一個列表,指出了匹配規則,很好理解就不說了。對於數字類型等也可以加入lt,gt等規則。

  構建了這樣一個視圖類之后,再訪問我們的API調試界面,可以看到會多出一個Filter的按鈕供我們調試過濾器,按下按鈕彈出的界面是各種條件(包括過濾和排序等),嘗試着寫一個過濾一下,觀察下URL規則吧。其實和django中是一樣的。比如一個uri可能是:/albums/?album_name__contains=肖邦&artist__contains=傑倫&ordering=-artist,這就把整個Album.objects.all()結果集縮圈到album_name字段中有“肖邦”二字,且artist值含有傑倫的結果集,並且給出的結果集按照artist字段的值降序排序。

  ●  關於filter和serializer之間的獨立性

  在使用了一段時間的rest_framework框架之后,對這個方面有些認識了。在ORM設計性比較強的應用中,我們可以直接繼承ModelSerializer這個類來方便地基於一個模型創建出一個序列器類。另一方面,也可以繼承FilterSet,方便地基於一個模型來創建一個過濾器類。而后這個過濾器類往往還要被當做成一個屬性賦予序列器類。也就是說這里面的serializer和filter兩個關聯起來了。在這個過程中,可以注意到,serializer和filter都基於了同一個模型所以總是下意識地覺得,filter定義的字段必須是和serializer相同或者是其一個子集。其實不然。兩者是互相獨立的。

  比如在serializer中我們可以在class Meta中寫出fields = "__all__",此時序列器類會自動讀取模型的所有字段並且基於此創建序列器。對於某些字段如外鍵或者要做一些額外處理的,我們可以在Meta前面寫上某個字段=某個Field來做特別處理。

  在filter中,定義的字段則是相對自由的。比如一般filter中會指定model=xxx,然后后面的fields其實除了serializer里面定義的那些字段之外,還可以寫一些符合邏輯但是沒有被包括進的字段。這些字段就可以在rest_framework構成的API中直接使用而不出錯的。比如filter的fields中指定for_client__clientno: ['exact']這樣一個字段。很明顯for_client是另一個模型的外鍵,而for_client__clientno可以經過一次外鍵關聯碰到那個模型的clientno字段。

 

  ●  重載初始結果集

  上面的視圖類中明確指出了queryset=Album.objects.all(),也就是整個結果集。但是有時候我們可能希望,可以根據請求上下文在過濾之前的就進行一次“預過濾”,這可以通過重載視圖類的get_queryset方法實現。比如不知道是否還記得,之前為了測試對象層面的權限控制,我們為Album增加了一個owner字段並連接到了User模型上。其實這個權限也可以在這里通過重載初始結果集的辦法來實現:

def get_queryset(self):
    user = self.request.user
    return user.albums.all()

 

   這樣子的話,比如我是admin登錄進來的,不論我怎么過濾,最多也只能看到owner是admin的Album對象而不會越界看到其他用戶的。

 ■  分頁

  有了過濾,分頁自然也是不能少的。我們可以通過重載視圖類的pagination_class屬性來指定一個分頁類實現分頁。分頁類可以用rest_framework已經預設好的一些類也可以自己實現。

  分頁分成很多種風格,首先來說下rest_framework為我們預設好的一些風格的分頁方式。比如:

  ●  LimitOffsetPagination

  rest_framework.pagination.LimitOffsetPagination這個分頁類幫助實現的是類似於Mysql中limit和offset方式的分頁。在API請求參數中加上GET參數limit=X&offset=Y的話,就可以把當前結果集縮圈成第Y+1個到第Y+X個的那X個結果。比如我們直接將其應用到某個ListAPIView中:

from rest_framework.pagination import LimitOffsetPagination
from rest_framework import generics

class ServerList(generics.ListCreateAPIView):
    queryset = Server.objects.all()
    serializer_class = ServerSerializer
    permission_classes = (IsAuthenticatedOrReadOnly,)
    authentication_classes = (SessionAuthentication,TokenAuthentication)
    filter_backends = (DjangoFilterBackend,OrderingFilter)
    filter_class = ServerFilter
    #設置pagination
    pagination_class = LimitOffsetPagination
    ordering = ('for_client__clientno',)

 

   此時我們訪問ServerList.as_view關聯的那個url時可以加上limit和offset的參數了。順便,在rest_framework的可視化調試界面上,帶有分頁地訪問時會出現一個分頁按鈕,可以點擊不同的頁碼跳到不同的頁。而且分頁比較智能,挺好用的。

  需要注意的是,加入pagination_class之后,當請求帶有分頁參數limit時這個類的返回不再是單純的[{...},{...}]這種形式的JSON了。而是類似下面這種:

{
    "count": 5,
    "previous": "xxx",
    "next": "yyy",
    "results": [
         {...},
         {...}
     ]
}

 

  previous和next的值其實是兩個url,分別指出當前分頁的上一頁和下一頁的請求url,非常貼心!results即這次請求得到的結果,count是指在未分頁時結果集的總長度。

 有些時候組件發出的請求自帶了limit(比如bootstrap-table在設置了pagination:true之后)參數,此時應該注意到返回結果的變化。

  ●  PageNumberPagination

 

■  去除web調試界面

  在完成API的編寫之后,上生產環境之前,應該把web的調試界面去除。雖然通過ajax發起的請求可以指定dataType參數為json而不為html,從而避免返回API調試界面html的問題,但是依然可能會存在比如表單提交,其他請求等場景下返回了html的情況。為了一勞永逸地禁用web調試界面,應該在settings.py中加入如下配置:

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
        #'rest_framework.renderers.BrowsableAPIRenderer' 這個就是調試界面的renderer
    )
}

 

  只要指出我們不要BrowsableAPIRenderer這個renderer那么就可以讓所有請求得到的都是JSON格式的數據了。

 

■  零散

  ●  patch等方法總是無法如願修改?

  按照JSON傳輸的標准,檢查ajax請求參數是否包含了下面幾項:

  traditional: true,

  contentType: 'application/json; charset=utf-8'

  以及data字段的object應該有JSON.stringify()包裹,如data: JSON.stringify({xxx})

  同時檢查上面提到過的驗證字段如headers中應當添加{'X-CSRFTOKEN': getCookie('csrftoken')}這樣的

 

  ●  關於rest_framework有多層外鍵時取數據的原則

  在通過rest_framework構建API的時候,對於List型的API(且不加任何filter參數條件)給出的數據,印象中感覺就像是select * from xxx這樣的。但其實rest_framework會對DB中實際存儲的數據,根據ORM的規則判斷來進行一個“預過濾”。即不符合ORM邏輯的數據rest_framework並不會給予展示,即便數據真實存在於DB中。

  比如針對如下這個模型:

class A(models.Model):
    for_B = models.ForeignKey(
        B,
        on_delete=models.CASCADE,
        related_name='has_A'
    )
    # ...

  由於某個B對象被刪除后所有相關的B.has_A.all()中的A對象都會被一並刪除,所以如果正確地按照ORM的邏輯來,A表里應該是不會存在某個記錄,其for_B_id字段的值在B表中不存在記錄。但是數據庫並未對這個邏輯做出限制。加入我們在A表中插入一條INSERT INTO A VALUES(.....,'999'),即for_B_id是999的記錄。如果B表中存在某個B對象的id是999還好說,如果沒有,那么這個A記錄就不會在通過rest_framework獲取A列表時出現。此時就會導致API獲得的數據條數和實際表中的記錄條數不一致。

  這一層控制是在rest_framework而不是ORM本身中做出的,因為對於這種記錄,如果在代碼中進行A.objects.all()的話還是會被選擇出來的。

   反過來,如果在模型定義中就設置了on_delete=models.SET_NULL,null=True的話,那么手動插入這么一條記錄后,rest_framework在獲取所有A對象的時候會給出這條記錄,並且其for_B字段的值是null

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM