drf 檢視使用及原始碼分析
前言
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
,在其之上做了一些封裝,使操作更加簡單。
封裝特性
在APIView
中對原生的request
物件進行封裝,最常用的兩個屬性如下,它彌補了Django
原生View
對JSON
請求格式的資料沒有處理的缺陷。
同時,APIView
認為對於GET
請求的資源引數,不應該使用GET
獲取,而是應該使用query_params
進行獲取。
屬性 | 描述 |
---|---|
request.data | 當請求資料為Json格式時,將以dict形式儲存,主要針對request.POST請求 |
request.query_params | 當請求方式為GET時,可獲取url中的請求資料 |
介面書寫
以下是使用APIView
對User
表進行增刪改查的介面書寫。
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)
問題發現
在上述程式碼中,問題有以下幾點:
- 重複程式碼多,在每個介面中都需要書寫
ORM
查詢 - 每個介面都需要針對同一個序列類做出不同的例項化
GenericAPIView
繼承關係
GenericAPIView
的匯入如下:
from rest_framework.generics import GenericAPIView
以下是它的繼承關係:
可以發現它是對APIView
的繼承,所以理論上來說應該又多了一些東西。
原始碼閱讀
下面來看一下GenericAPIView
的原始碼,首先你可以發現大概有4個類屬性:
class GenericAPIView(views.APIView):
queryset = None # 要查詢的資料表
serializer_class = None # 執行序列化的序列化類
lookup_field = 'pk' # 查詢時的查詢條件,預設按主鍵查詢
lookup_url_kwarg = None # 如果在檢視中,url捕獲的查詢資料表過濾引數不是pk,你應該進行宣告
接著往下看,其實它的方法很少,對外暴露的方法就更少了。
我們這裡就先看最常用的,即對外暴露的方法,首先是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_field
為name
。
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
,是獨立的子類。
原始碼閱讀
下面是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)
至於其他幾個類,其實都差不多,這裡摘出兩個比較特別的類來看一下,分別是CreateModelMixin
與DestroyModelMixin
這兩個類。
下面是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()
,所以我們就不用再對返回物件進行包裝了。
介面書寫
下面是利用GenericAPIView
與mixins
中的五個擴充套件類進行介面書寫。
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
是針對GenericAPIView
與mixins
中擴充套件類的結合使用做了一些優化,它可以根據不同的請求自動的做出迴應。
同時也不再需要你在檢視中進行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
下面是它的繼承圖:
那麼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
我們可以在上面基本使用時對url
中as_view()
傳參發現了一點不一樣的地方,我們傳遞進了一個關鍵字引數actions
,這個引數非常的蹊蹺,因為在APIView
中的as_view()
方法中並沒有為該引數預留位置。
def as_view(cls, **initkwargs):
我們再接著看看GenericAPIView
中的as_view()
方法有沒有為該引數預留位置,非常遺憾的是在GenericAPIView
中根本就沒有as_view()
方法,說明它用了父類也就是APIView
的as_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'})),