drf 檢視使用及原始碼分析

雲崖先生發表於2020-10-28

前言

   drf檢視的原始碼非常的繞,但是實現的功能卻非常的神奇。

   它能夠幫你快速的解決ORM增刪改查的重複程式碼,非常的方便好用。

   下面是它原始碼中的一句話:

class ViewSetMixin:
    """
    This is the magic.
    """

   好了,屁話不多說,直接看看drf檢視中的功能吧。

準備工作

   此次的Django採用3版本,所以相對於1版本來說有一些差異。

模型表

   下面是模型表:

from django.db import models

# Create your models here.
class User(models.Model):
    user_id = models.AutoField(primary_key=True)
    user_name = models.CharField(max_length=50)
    user_gender = models.BooleanField(
        [(0,"male"),(1,"female")],
        default = 0,
    )
    user_age = models.IntegerField()

    def __str__(self):
        return self.user_name

    class Meta:
        db_table = ''
        managed = True
        verbose_name = 'User'
        verbose_name_plural = 'Users'

   資料如下:

INSERT INTO app01_user(user_name,user_age,user_gender) VALUES
    ("使用者1",18,0),
    ("使用者2",19,1),
    ("使用者3",19,1);

序列類

   序列類採用ModelSerializer

from rest_framework import serializers
from . import models

class UserModelSerializers(serializers.ModelSerializer):
    
    class Meta:
        model = models.User
        fields = "__all__"

url路由

   下面是url路由的設定:

re_path('^api/users/(?P<uid>\d+)?',views.UserAPI.as_view())

封裝Rsponse

   由於不需要返回原生的Response,所以我們封裝了一個類,用於更加方便的返回Response

class ResponseMeta(type):
    # 對Response類做封裝
    def __call__(cls, *args, **kwargs):
        obj = cls.__new__(cls, *args, **kwargs)
        cls.__init__(obj, *args, **kwargs)
        return Response(data=obj.__dict__)


class CommonResponse(object, metaclass=ResponseMeta):
    # 返回的資訊
    def __init__(self, status, data=None, errors=None):
        self.status = status
        self.data = data
        self.errors = errors

APIView

繼承關係

   APIView的匯入如下:

from rest_framework.views import APIView

   APIView繼承了原生的DjangoView,在其之上做了一些封裝,使操作更加簡單。

   image-20201028161439038

封裝特性

   在APIView中對原生的request物件進行封裝,最常用的兩個屬性如下,它彌補了Django原生ViewJSON請求格式的資料沒有處理的缺陷。

   同時,APIView認為對於GET請求的資源引數,不應該使用GET獲取,而是應該使用query_params進行獲取。

屬性描述
request.data 當請求資料為Json格式時,將以dict形式儲存,主要針對request.POST請求
request.query_params 當請求方式為GET時,可獲取url中的請求資料

介面書寫

   以下是使用APIViewUser表進行增刪改查的介面書寫。

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.request import Request
from . import models
from . import ser


class ResponseMeta(type):
    # 對Response類做封裝
    def __call__(cls, *args, **kwargs):
        obj = cls.__new__(cls, *args, **kwargs)
        cls.__init__(obj, *args, **kwargs)
        return Response(data=obj.__dict__)


class CommonResponse(object, metaclass=ResponseMeta):
    # 返回的資訊
    def __init__(self, status, data=None, errors=None):
        self.status = status
        self.data = data
        self.errors = errors


class UserAPI(APIView):
    def get(self, request, uid=None):
        if not uid:
            # 獲取所有
            user_queryset = models.User.objects.all()
            if user_queryset.exists():
                serialization = ser.UserModelSerializers(instance=user_queryset, many=True)
                return CommonResponse(status=100, data=serialization.data, errors=None)
            return CommonResponse(status=200, errors="暫時沒有任何學生")
        else:
            user_obj = models.User.objects.filter(pk=uid).first()
            if user_obj:
                serialization = ser.UserModelSerializers(instance=user_obj)
                return CommonResponse(status=100, data=serialization.data, errors=None)
            return CommonResponse(status=200, errors="沒有該學生")

    def post(self, request):

        serialization = ser.UserModelSerializers(data=request.data)
        if serialization.is_valid():
            serialization.save()
            return CommonResponse(status=100, data=serialization.data, errors=None)
        return CommonResponse(status=200, errors=serialization.errors)

    def patch(self, request, uid):
        user_obj = models.User.objects.filter(pk=uid).first()
        if user_obj:
            serialization = ser.UserModelSerializers(instance=user_obj, data=request.data)
            if serialization.is_valid():
                serialization.save()
                return CommonResponse(status=100, data=serialization.data, errors=None)
            else:
                return CommonResponse(status=200, errors="修改失敗,請檢查欄位是否一直")
        else:
            return CommonResponse(status=200, errors="修改失敗,請檢查該使用者是否存在")

    def delete(self,request,uid):
        models.User.objects.get(pk=uid).delete()
        return CommonResponse(status=100,data="刪除成功",errors=None)

問題發現

   在上述程式碼中,問題有以下幾點:

  1. 重複程式碼多,在每個介面中都需要書寫ORM查詢
  2. 每個介面都需要針對同一個序列類做出不同的例項化

GenericAPIView

繼承關係

   GenericAPIView的匯入如下:

from rest_framework.generics import GenericAPIView

   以下是它的繼承關係:

   image-20201028161722179

   可以發現它是對APIView的繼承,所以理論上來說應該又多了一些東西。

原始碼閱讀

   下面來看一下GenericAPIView的原始碼,首先你可以發現大概有4個類屬性:

class GenericAPIView(views.APIView):

    queryset = None  # 要查詢的資料表
    serializer_class = None  # 執行序列化的序列化類

    lookup_field = 'pk'  # 查詢時的查詢條件,預設按主鍵查詢
    lookup_url_kwarg = None  # 如果在檢視中,url捕獲的查詢資料表過濾引數不是pk,你應該進行宣告

   接著往下看,其實它的方法很少,對外暴露的方法就更少了。

   image-20201028142929380

   我們這裡就先看最常用的,即對外暴露的方法,首先是get_queryset()

    def get_queryset(self):

        assert self.queryset is not None, (
            "'%s' should either include a `queryset` attribute, "
            "or override the `get_queryset()` method."
            % self.__class__.__name__
        )  # 做驗證,即例項屬性queryset不能是空,代表這個類屬性你必須要宣告,你可以選擇將它做成類屬性也可以做成例項屬性

        queryset = self.queryset  # 進行賦值,將self.queryset賦值為類屬性。先在UserAPI的例項中找,找不到再到UserAPI的類中找
        if isinstance(queryset, QuerySet):
            queryset = queryset.all()  # 如果它是一個QuerySET物件,就獲取全部,得到一個QuerySetDict物件
        return queryset  # 進行返回

   看到這裡發現了一個點,即queryset這個屬性必須要進行賦值,由於屬性查詢順序是先查詢例項,而後查詢類本身,所以我們直接在UserAPI中宣告queryset為類屬性即可。

   接下來繼續繼續看,get_object(),見明知意,它可以從資料表中獲取單個物件:

    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())  # 首先會執行get_queryset(),獲取一個所有物件的列表,然後進行filter過濾
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field  # 這裡是對url中獲取到的變數進行對映

        assert lookup_url_kwarg in self.kwargs, (  # 比如,url中獲取到的名為uid,如果uid沒有在kwargs中,即是{uid:4}中,則丟擲異常
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}  # 進行查詢替換,預設的self.lookup_field是pk,將uid替換為pk。那麼這裡就是{pk:4}
        obj = get_object_or_404(queryset, **filter_kwargs) # 獲取物件
        self.check_object_permissions(self.request, obj)  # 進行驗證許可權

        return obj  # 返回單個物件

   看到這裡就發現了,預設查詢條件是用pk,也就是說你的url中必須要用pk這個形參名進行分組捕獲。否則就需要宣告lookup_url_kwarg,即lokup_url_kwarg="uid",然後進行替換組建filter_kwargs。當然如果你的查詢條件不是用的pk,就需要修改lookup_field為欄位名,如我不是按照pk進行查詢,而是按照name,就修改lookup_fieldname

re_path('^api/users/(?P<uid>\d+)?',views.UserAPI.as_view())

   接下來再看另一個方法get_serializer()

   def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()  # 內部調get_serializer_class
        kwargs.setdefault('context', self.get_serializer_context())  # 獲取context屬性
        return serializer_class(*args, **kwargs)  # 呼叫serializer_class並返回

   接下來是get_serializer_class()方法:

    def get_serializer_class(self):
    
        assert self.serializer_class is not None, (  # 傳入的必須不能是None
            "'%s' should either include a `serializer_class` attribute, "
            "or override the `get_serializer_class()` method."
            % self.__class__.__name__
        )

        return self.serializer_class  # 返回設定的屬性,serializer_class,即序列化類

   OK,其實原始碼讀到這裡就行了。

封裝特性

   通過上面的原始碼分析,總結出如下方法的使用:

方法/屬性描述
queryset 將要查詢的資料表,型別應該是QuerySet
serializer_class 將要執行的序列化類,型別不能為None
lookup_field 查詢時的查詢條件,預設為pk
lookup_url_kwarg 檢視中url捕獲的查詢條件變數名如果不是pk,則應該進行指定
get_queryset() 查詢獲取所有記錄
get_object() 查詢獲取單條記錄
get_serializer() 執行序列化物件

   關於最常用的呼叫方法就三個,常用屬性四個。

   其他的方法基本上都是內部呼叫,所以暫時不深究。

介面書寫

   接下來使用GenericAPIView進行介面書寫。

from rest_framework.response import Response
from rest_framework.generics import GenericAPIView
from . import models
from . import ser


class ResponseMeta(type):
    # 對Response類做封裝
    def __call__(cls, *args, **kwargs):
        obj = cls.__new__(cls, *args, **kwargs)
        cls.__init__(obj, *args, **kwargs)
        return Response(data=obj.__dict__)


class CommonResponse(object, metaclass=ResponseMeta):
    # 返回的資訊
    def __init__(self, status, data=None, errors=None):
        self.status = status
        self.data = data
        self.errors = errors


class UserAPI(GenericAPIView):
    queryset = models.User.objects  # 傳入物件即可
    serializer_class = ser.UserModelSerializers  # 序列化類
    lookup_field = "pk"
    lookup_url_kwarg = "uid"  # 由於捕獲的是uid,需要宣告

    def get(self, request, uid=None):
        if not uid:
            # 獲取所有
            user_queryset = self.get_queryset()  # 獲取所有
            if user_queryset.exists():
                serialization = self.get_serializer(instance=user_queryset,many=True)  # 獲取序列化類,序列化多條
                return CommonResponse(status=100, data=serialization.data, errors=None)
            return CommonResponse(status=200, errors="暫時沒有任何學生")

        else:
            user_obj = self.get_object()
            if user_obj:
                serialization = self.get_serializer(instance=user_obj)
                return CommonResponse(status=100, data=serialization.data, errors=None)
            return CommonResponse(status=200, errors="沒有該學生")

    def post(self, request):

        serialization = self.get_serializer(data=request.data)
        if serialization.is_valid():
            serialization.save()
            return CommonResponse(status=100, data=serialization.data, errors=None)
        return CommonResponse(status=200, errors=serialization.errors)

    def patch(self, request, uid):
        user_obj = self.get_object()
        if user_obj:
            serialization = self.get_serializer(instance=user_obj,data=request.data)
            if serialization.is_valid():
                serialization.save()
                return CommonResponse(status=100, data=serialization.data, errors=None)
            else:
                return CommonResponse(status=200, errors="修改失敗,請檢查欄位是否一直")
        else:
            return CommonResponse(status=200, errors="修改失敗,請檢查該使用者是否存在")

    def delete(self,request,uid):
        self.get_object().delete()
        return CommonResponse(status=100,data="刪除成功",errors=None)

問題發現

   相對於使用APIView來說,它不必再手動去寫ORM語句。

   但是對於返回資訊、對於驗證操作還是要自己寫。

mixins中擴充套件類

五個擴充套件類

   下面是rest_framework.mixins中的五個擴充套件類,它們做了更高階別的封裝,配合GenericAPIView使用有奇效。

from rest_framework.mixins import ListModelMixin,RetrieveModelMixin,CreateModelMixin,UpdateModelMixin,DestroyModelMixin
描述
ListModelMixin 該類主要負責查詢所有記錄
RetrieveModelMixin 該類主要負責查詢單條記錄
CreateModelMixin 該類主要負責建立記錄
UpdateModelMixin 該類主要負責對記錄做更新操作
DestroyModelMixin 該類主要負責刪除記錄

繼承關係

   這五個類都繼承於object,是獨立的子類。

   image-20201028171401608

原始碼閱讀

   下面是ListModelMixin的原始碼,不難發現,它就是配合GenericAPIView使用的,因為它會使用get_queryset()方法,並且,它會自動的返回Response物件,並把驗證結果新增進去:

class ListModelMixin:

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

   至於其他幾個類,其實都差不多,這裡摘出兩個比較特別的類來看一下,分別是CreateModelMixinDestroyModelMixin這兩個類。

   下面是CreateModelMixin類的原始碼:

class CreateModelMixin:

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)  # 內部進行序列化類儲存
        headers = self.get_success_headers(serializer.data)  # 返回一個location請求頭
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)  # 注意返回結果,狀態碼是201

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

   下面是DestroyModelMixin類的原始碼:

class DestroyModelMixin:
    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)  # 內部執行刪除
        return Response(status=status.HTTP_204_NO_CONTENT)  # 返回狀態碼204

    def perform_destroy(self, instance):
        instance.delete()

   那麼讀這兩個類的原始碼,就是想要讓你知道,建立成功後的返回狀態碼是201,而刪除成功的返回狀態碼是204。這在REST規範中寫的很清楚,可以看見這裡也是這麼做的。

類與方法

   下面是不同的五個擴充套件類中不同的五個方法,功能與類一樣。

方法描述
ListModelMixin list() 查詢所有,並返回Response物件
RetrieveModelMixin retrieve() 查詢單條,並返回Response物件
CreateModelMixin create() 建立記錄,並返回Response物件
UpdateModelMixin update() 更新記錄,並返回Response物件
DestroyModelMixin destroy() 刪除記錄,並返回Response物件

   由於它會自動進行return Response(),所以我們就不用再對返回物件進行包裝了。

介面書寫

   下面是利用GenericAPIViewmixins中的五個擴充套件類進行介面書寫。

from . import models
from . import ser
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import ListModelMixin,RetrieveModelMixin,CreateModelMixin,UpdateModelMixin,DestroyModelMixin

class UserAPI(GenericAPIView,ListModelMixin,RetrieveModelMixin,CreateModelMixin,UpdateModelMixin,DestroyModelMixin):

    queryset = models.User.objects  # 傳入物件即可
    serializer_class = ser.UserModelSerializers  # 序列化類
    lookup_field = "pk"
    lookup_url_kwarg = "uid"  # 由於捕獲的是uid,需要宣告

    def get(self, request, uid=None):
        if not uid:
            # 獲取所有
            return self.list(request)

        else:
            return self.retrieve(request,uid)

    def post(self, request):
        return self.create(request)

    def patch(self, request, uid):
        return self.update(request,uid)

    def delete(self,request,uid):
        return self.destroy(request,uid)

問題發現

   可以看見,程式碼相比於前兩個少了非常非常多。但是還是存在一些問題。

   第一個問題就是這個檢視UserAPI繼承的類太多了,太長了,其次就是每次都需要在檢視中return,它能不能幫我們自己return呢?那這個就非常舒服了。

modelViewSet

基本使用

   modelViewSet是針對GenericAPIViewmixins中擴充套件類的結合使用做了一些優化,它可以根據不同的請求自動的做出回應。

   同時也不再需要你在檢視中進行return。以下是基本使用方法,但是使用它時我們需要對路由做一些改進,具體的情況等下面的原始碼分析後你就明白了:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', views.UserAPI.as_view(actions={"get":"list","post":"create"})),
    re_path('^api/users/(?P<uid>\d+)?',views.UserAPI.as_view(actions={"get":"retrieve","patch":"update","delete":"destroy"}))
]

   那麼在views.py中,書寫的話很簡單:

from . import models
from . import ser
from rest_framework.viewsets import ModelViewSet
class UserAPI(ModelViewSet):
    queryset = models.User.objects  # 傳入物件即可
    serializer_class = ser.UserModelSerializers  # 序列化類
    lookup_field = "pk"
    lookup_url_kwarg = "uid"  # 由於捕獲的是uid,需要宣告

繼承關係

   ModelViewSet的匯入如下:

from rest_framework.viewsets import ModelViewSet

   你可看它的原始碼,它其實也沒什麼特別之處,就是針對上面第一個問題做了改進。但是你會發現,它會繼承一個新的類,即GenericViewSet這個類。

class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass

   下面是它的繼承圖:

   image-20201028174903534

   那麼GenericViewSet中又會有什麼新的發現呢?我們先看一看它。

GenericViewSet

   開啟GenericViewSet中發現什麼都沒有。但是它繼承了ViewSetMixin

class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
    """
    The GenericViewSet class does not provide any actions by default,
    but does include the base set of generic view behavior, such as
    the `get_object` and `get_queryset` methods.
    """
    pass

ViewSetMixin

   我們可以在上面基本使用時對urlas_view()傳參發現了一點不一樣的地方,我們傳遞進了一個關鍵字引數actions,這個引數非常的蹊蹺,因為在APIView中的as_view()方法中並沒有為該引數預留位置。

def as_view(cls, **initkwargs):

   我們再接著看看GenericAPIView中的as_view()方法有沒有為該引數預留位置,非常遺憾的是在GenericAPIView中根本就沒有as_view()方法,說明它用了父類也就是APIViewas_view()方法

   那麼只有一個可能,就是ViewSetMixin覆寫了as_view()方法,那麼到底是不是這麼回事?我們看一下就知道了:

class ViewSetMixin:

    @classonlymethod
    def as_view(cls, actions=None, **initkwargs):

   是的,那麼它內部是怎麼做的呢?實際上它的核心程式碼就是那個for迴圈,它會根據不同的請求方式來執行不同的mixins中五個擴充套件類的方法,因此我們需要兩條url來放入不同的actions。由於modelsViewSet繼承了mixins五個擴充套件類,所以才能夠呼叫擴充套件類下的方法。

    @classonlymethod 
    def as_view(cls, actions=None, **initkwargs):  # cls即為UserAPI這個類
        cls.name = None
        cls.description = None
        cls.suffix = None
        cls.detail = None
        cls.basename = None
        if not actions:  # 必須傳入actions,否則丟擲異常
            raise TypeError("The `actions` argument must be provided when "
                            "calling `.as_view()` on a ViewSet. For example "
                            "`.as_view({'get': 'list'})`")
        for key in initkwargs:  # 構造字典,不用管
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r" % (
                    cls.__name__, key))

        if 'name' in initkwargs and 'suffix' in initkwargs:  # 不用管,這個也是構造字典
            raise TypeError("%s() received both `name` and `suffix`, which are "
                            "mutually exclusive arguments." % (cls.__name__))

        def view(request, *args, **kwargs):  # 閉包函式view
            self = cls(**initkwargs)

            if 'get' in actions and 'head' not in actions:
                actions['head'] = actions['get']

            self.action_map = actions

            for method, action in actions.items():  # 其實這裡是核心程式碼,  actions={"get":"retrieve","patch":"update","delete":"destroy"},或者等於{"get":"list","post":"create"}
                handler = getattr(self, action)  # 根據請求方式,來執行list、create、retrieve、update、destroy這幾個方法
                setattr(self, method, handler)

            self.request = request
            self.args = args
            self.kwargs = kwargs

            return self.dispatch(request, *args, **kwargs)
 
        update_wrapper(view, cls, updated=())  # 傳入執行update_wrapper(),不用管

        update_wrapper(view, cls.dispatch, assigned=())   # 不用管


        view.cls = cls
        view.initkwargs = initkwargs
        view.actions = actions
        return csrf_exempt(view)

   根據不同的請求方式來執行不同的函式方法,可以說這個設計非常的巧妙,所以你可以像下面這樣做:

# views.py
from rest_framework.viewsets import ViewSetMixin
class Book6View(ViewSetMixin,APIView): # 一定要放在APIVIew前,因為as_view()的查詢順序一定要先是ViewSetMixin
    def get_all_book(self,request):
        print("xxxx")
        book_list = Book.objects.all()
        book_ser = BookSerializer(book_list, many=True)
        return Response(book_ser.data)
    
# urls.py
    #繼承ViewSetMixin的檢視類,路由可以改寫成這樣
    path('books6/', views.Book6View.as_view(actions={'get': 'get_all_book'})),

相關文章