【Django drf】檢視層大總結 ViewSetMixin原始碼分析 路由系統 action裝飾器

passion2021發表於2023-02-07

九個檢視子類

# 兩個檢視基類 
1.APIView       2.GenericAPIView
APIView:       renderer_classes響應格式類 parser_classes請求解析類    跟資料庫解耦合
GenericAPIView:queryset資料集 serializer_class序列化類                跟資料庫耦合

# 5個檢視擴充套件類 (提供方法)
ListModelMixin      -->  list      -->  查詢所有
RetrieveModelMixin  -->  retrieve  -->  查詢一個
CreateModelMixin    -->  create    -->  新增一個
UpdateModelMixin    -->  update    -->  修改一個
DestroyModelMixin   -->  destroy   -->  刪除一個

# 9個檢視子類 
繼承關係公式: 檢視子類 = n * 檢視擴充套件類 + GenericAPIView 

# 示例:
ListAPIView     =  ListModelMixin     + GenericAPIView 
RetrieveAPIView =  RetrieveModelMixin + GenericAPIView 
CreateAPIView   =  CreateModelMixin   + GenericAPIView 
...
RetrieveDestroyAPIView = RetrieveModelMixin + DestroyModelMixin + GenericAPIView 
RetrieveUpdateDestroyAPIView = RetrieveModelMixin + UpdateModelMixin + DestroyModelMixin + GenericAPIView

'''
總結:9個檢視子類都繼承GenericAPIView
'''

使用檢視子類寫五個介面:這裡上一節講過,所以不再贅述。

## 路由
urlpatterns = [
    path('books/', views.BookView.as_view()),
    path('books/<int:pk>/', views.BookView.as_view()),
]

# 檢視類
class BookView(ListCreateAPIView):  # 查詢所有,新增一個
    queryset = Book.objects.all()
    serializer_class = BookSerializer


class BookDetailView(RetrieveUpdateDestroyAPIView): # 新增一個,修改一個,刪除一個
    queryset = Book.objects.all()
    serializer_class = BookSerializer

以後可能只希望寫某幾個介面,而不是全部介面都存在,
可以透過繼承不同的檢視類實現。

只要查詢所有和刪除一個,怎麼寫?
示例:
image-20230206095158841

為什麼沒有Destroy和Updata的組合?
因為必須先查出來,再修改或刪除,所以沒有這個組合。

之後會繼續封裝:兩個檢視類 ---> 一個檢視類
問題:
1.有兩個get請求對應一個CBV中get方法
2.兩個路由路徑對應一個CBV

檢視集

繼承ModelViewSet類寫五個介面

# 路由
urlpatterns = [
    path('books/', views.BookView.as_view({'get': 'list', 'post': 'create'})),
    path('books/<int:pk>/', views.BookView.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
]

# 檢視類
class BookView(ModelViewSet):  # 查詢所有,新增一個
    queryset = Book.objects.all()
    serializer_class = BookSerializer

檢視ModelViewSet內部繼承關係:

image-20230206190140691

從註釋也可以看出來他繼承了 5個檢視擴充套件類,也就是說ModelViewSet內部具備所有的"動作",也就是例如:
create()list()update()retrieve()destroy()這些方法
但是我們請求來了,還是會呼叫檢視類中的get()post()put()delete()這些方法呀。
比如一個擴充套件子類:ListAPIView

image-20230206191121535

他的內部就是寫了get方法,我們get請求來了之後,就會呼叫這個方法,然後再去呼叫父類的list方法。

ModelViewSet內部居然沒有寫,這是怎麼回事?
這是因為ModelViewSet繼承的最後一個類GenericViewSet,這是一個魔法類,他重寫了as_view。

我們直接發個請求執行一下。

image-20230206095731504

會發現如下報錯:

image-20230206095821074

可以得知,一旦繼承ModelViewSet,路由層的寫法就變了!
現在需要這樣寫:

image-20230206100143663

這樣寫的意思是:

  • 對於books/這個路由:
    get請求 --執行--> list方法
    post請求 --執行--> create方法

  • 對於books/<int:pk>/這個路由:
    get請求 --執行--> retrieve方法
    put請求 --執行--> updata方法
    delete請求 --執行--> destroy方法

先記住這個格式,知道怎麼用,後續原始碼分析再詳細瞭解。

繼承 ReadOnlyModelView編寫2個只讀介面

# 路由
urlpatterns = [
    path('books/', views.BookView.as_view({'get': 'list'})),
    path('books/<int:pk>/', views.BookView.as_view({'get': 'retrieve'})),
]

# 檢視類
class BookView(ReadOnlyModelViewSet):  # 查詢所有,新增一個
    queryset = Book.objects.all()
    serializer_class = BookSerializer

檢視 readonlymodelview內部繼承關係:

image-20230206191921877

這個類中只有list方法和retrieve方法。他同樣繼承了魔法類。
所以繼承這個類就只能寫兩個只讀介面:查詢所有、查詢一個

ViewSetMixin原始碼分析

檢視GenericViewSet繼承關係:

image-20230206192034159

ViewSetMixin是個魔法類,重寫了as_view:

image-20230206100957863

查詢as_view方法

路由寫法為什麼變了?
導致路由寫法變了的原因是: ViewSetMixin
當請求來了之後,會執行ViewSetMixin類中的as_view方法的返回值。

# 請求來了,路由匹配成功---》get請求,匹配成功books,會執行  views.BookView.as_view({'get': 'list', 'post': 'create'})()------>讀as_view【這個as_view是ViewSetMixin的as_view】

從路由層開始分析,根據繼承屬性一個一個找as_view方法(從左往右)
image-20230206101341886

ListModelMixinRetrieveModelMixinCreateModelMixinUpdateModelMixin DestroyModelMixin這些方法中都沒有as_view。
所以會進入到GenericViewSet:

image-20230206101416150

GenericViewSet的第一個父類是ViewSetMixin。

所以會先執行ViewSetMixin的as_view():

@classonlymethod
def as_view(cls, actions=None, **initkwargs):
    # 如果沒有傳actions,直接拋異常,路由寫法變了後,as_view中不傳字典,直接報錯
    if not actions:
        raise TypeError("The `actions` argument must be provided when "
                        "calling `.as_view()` on a ViewSet. For example "
                        "`.as_view({'get': 'list'})`")
	# 。。。。其他程式碼不用看
    def view(request, *args, **kwargs):
        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():
            handler = getattr(self, action)
            setattr(self, method, handler)

        return self.dispatch(request, *args, **kwargs)
    # 去除了csrf校驗
    return csrf_exempt(view)

如果不給actions傳引數,直接丟擲異常。
也就是不給as_view()傳字典,就會丟擲異常。
as_view執行完後會返回內層函式view:(這裡執行的view是去除了csrf校驗的)

# 路由匹配成功執行views.BookView.as_view({'get': 'list', 'post': 'create'})()----》本質執
行ViewSetMixin----》as_view----》內的view()---》程式碼貼過來
    def view(request, *args, **kwargs):
            #actions 是傳入的字典--->{'get': 'list', 'post': 'create'}
            self.action_map = actions
            # 第一次迴圈:method:get,action:list
            # 第一次迴圈:method:post,action:create
            for method, action in actions.items():
                # 反射:去檢視類中反射,action對應的方法,action第一次是list,去檢視類中反射list方法
                # handler就是檢視類中的list方法
                handler = getattr(self, action)
                # 反射修改:把method:get請求方法,handler:list
                # 檢視類的物件的get方法,變成了list
                setattr(self, method, handler)

            return self.dispatch(request, *args, **kwargs) #dispatch是APIView的
        
# 關於這裡self.dipatch的說明
self.dipatch是APIView的dispatch
'''
self.dipatch --進行--> 封裝新request, 執行三大認證 --呼叫--> django view的dispatch
'''
# 關於反射的總結
	反射得到的是我們繼承的List create方法
	反射修改物件的屬性 比如將get方法修改為存放list方法
	最後的dispatch作用是獲取你寫的CBV類中的get方法(此時get方法 --> list方法)。
	魔法類可以修改物件中的屬性所指向的方法。
        
 # 關於整體的總結:
	-1 只要繼承ViewSetMixin的檢視類,路由寫法就變了(重寫了as_veiw)
    -2 變成需要需要傳入字典對映方法:{'get': 'list', 'post': 'create'}
    	-只要傳入actions,以後訪問get就是訪問list,訪問post,就是訪問create
    -3 其他執行跟之前一樣 
    -4 以後檢視類類中的方法名,可以任意命名,只要在路由中做好對映即可【重要】
    


setattr修改物件的屬性

實際上ModelViewSet中根本沒有get方法,我們透過setattr給CBV的物件新增了一個get屬性,裡面存放的就是list方法。
而這個list方法又是透過反射在CBV的父類獲取到的。所以就產生了這麼神奇的效果。
我們也可以在自己的CBV中重寫list方法,這樣getattr獲取到的就是我們重寫的list方法,然後get請求來了之後,也會執行我們重寫的這個list。重寫list之後,建議使用super方法呼叫一下父類的list,這樣就可以在父類list的基礎上,新增一些功能。

# 示例:
def token_auth(func):
    def inner(self, request, *args, **kwargs):
        token = request.query_params.get('token')
        token_exist = UserToken.objects.filter(token=token)
        if token_exist:
            res = func(self, request, *args, **kwargs)
            return res
        else:
            return Response({'code': 100, 'msg': '請先登入'})

    return inner

class BookView(ModelViewSet):  # 針對 獲取一個 修改一個 刪除一個 介面新增token驗證 
    queryset = Book.objects
    serializer_class = BookSerializer
    
	@token_auth
    def retrieve(self, request, *args, **kwargs):  
        res = super().retrieve(request, *args, **kwargs)
        return res
    
	@token_auth
    def update(self, request, *args, **kwargs):
        res = super().update(request, *args, **kwargs)
        return res
    
	@token_auth
    def destroy(self, request, *args, **kwargs):
        res = super().update(request, *args, **kwargs)
        return res

from rest_framework.viewsets包下的類


# from rest_framework.viewsets下有這幾個類:

ViewSetMixin:魔法類,重寫了as_view,只要繼承他,以後路由寫法變成了對映方法
ModelViewSet: 5個檢視擴充套件類 + ViewSetMixin(魔法類) + GenericAPIView
ReadOnlyModelViewSet: 2個檢視擴充套件類 + ViewSetMixin(魔法類) + GenericAPIView   只讀的兩個
ViewSet:ViewSetMixin(魔法類)  + APIView
GenericViewSet:ViewSetMixin(魔法類) + GenericAPIView

# 重點
	以後,你想繼承APIView,但是想變路由寫法【檢視類中方法名任意命名】,要繼承ViewSet
    以後,你想繼承GenericAPIView,但是想變路由寫法【檢視類中方法名任意命名】,要繼承GenericViewSet
    
# 總結
只要想變路由,就要繼承ViewSetMixin,但是ViewSetMixin不是CBV檢視類,他沒有list,create等方法,所以要配合APIView, GenericAPIView一起使用,所以會出現ViewSet,GenerucViewSet,幫助我們繼承好了。
ViewSet:       ViewSetMixin(魔法類)  + APIView
GenericViewSet:ViewSetMixin(魔法類)  + GenericAPIView

檢視層大總結

# 1. 兩個檢視基類
	-APIView,GenericAPIView
# 2. 5個檢視擴充套件類,不是檢視類,必須配合GenericAPIView

# 3. 9個檢視子類,是檢視類,只需要繼承其中某一個即可

# 4. 檢視集 
	-ModelViewSet:路由寫法變了,只需要寫兩行,5個介面都有了
    -ReadOnlyModelViewSet:路由寫法變了,只需要寫兩行,2個只讀介面都有了
    -ViewSetMixin:不是檢視類,魔法,重寫了as_view,路由寫法變了,變成對映了
    	views.BookView.as_view({'get': 'list', 'post': 'create'})
    -ViewSet:ViewSetMixin+ APIView
	-GenericViewSet:ViewSetMixin+ GenericAPIView

    
    
# 舉例子:傳送簡訊介面,檢視類叫SendView,方法叫send_sms,路由配置變了
	get--->send_sms
	class SendView(ViewSet):
        def send_sms(self,request):
            

image-20230206203301634

任意命名檢視類的方法

在檢視類寫的方法可以任意命名,只要在路由層的字典寫好對映關係就行。

image-20230206102908998

只要想變路由,就要繼承ViewSetMixin,但是ViewSetMixin不是CBV檢視類,他沒有list,create等方法,所以要配合APIView,GenericAPIView一起使用,所以會出現ViewSet,GenerucViewSet,幫助我們繼承好了。

如何選擇檢視類

  • 為什麼要使用APIview?

    對於傳送簡訊的介面,
    其不跟資料庫打交道:繼承ViewSet
    ViewSet = 魔法類 + APIView
    因為APIView不需要配置queryset和序列化類
    繼承GenericViewSet會查資料庫,這是一種資源的浪費。
    所以跟資料庫打交道:繼承GenericViewSet
    GenericViewSet = 魔法類 + GenericAPIview

  • 有沒有推薦的檢視類組合?

    9個檢視子類 + 魔法類
    因為通常我們對一個資料庫資源比如:user
    對於這些資料資源,我們不一定會提供全部介面,很可能只會寫其中的幾個介面。

路由系統

路由寫法的三種情況

# drf 由於繼承ViewSetMinxin類,路由寫法變了
	-原生+drf,以後的路由寫法,可能會有如下情況(三種情況)
    	-path('books/', views.BookView.as_view()  
              # 原生django寫法
        -path('books/', views.BookView.as_view({'get': 'list', 'post': 'create'}))
              # 魔法類路由寫法
        -自動生成 ---> 還有擴充套件

路由類的使用

使用路由類是為了自動生成路由。

# drf提供了兩個路由類,繼承ModelViewSet後,路由可以自動生成
              
# 使用步驟:
    # 第一步:匯入路由類
    # 第二步,例項化得到物件(兩個類,一般使用SimpleRouter)
    # 第三步:註冊:router.register('books', views.BookView, 'books')
    # 第四步:在urlpatterns中註冊,兩種方式
        -urlpatterns += router.urls
        -include:path('/api/v1/', include(router.urls))  方式多一些
                            
# 底層實現:自動生成路由就
       -本質是自動做對映,能夠自動成的前提是,檢視類中要有 5個方法的某要給或多個
           get--->list
           get---->retrieve
           put---->update
           post---->create
           delete---->destory
       -ModelViewSet,ReadOnlyModelViewSet可以自動生成
              
       -9個試圖子類+配合ViewSetMixin   才可以自動生成
       -GenericAPIView+5個試圖擴充套件類+配合ViewSetMixin   才能自動生成

使用步驟

第一步:匯入路由類 使用simplerouter 就生成兩個路由 使用DefaultRouter -->生成的路由更多

image-20230206110849796

第二步:例項化得到物件。

image-20230206110940792

第三步:註冊路由。路徑和檢視類建立關係 有幾個檢視類就要寫幾次

image-20230206111102247

第四步:在urlpatterns註冊

image-20230206111222189

也就是將生成好的路由,新增到urlpatterns列表。

使用SimpleRouter(常用)

from rest_framework.routers import SimpleRouter

router =  SimpleRouter()
router.register('books', views.BookView, 'books')

urlpatterns = [
    path('admin/', admin.site.urls),
]
urlpatterns += router.urls

關於router.register:

第一個引數:具體路由地址 (會自動幫我們加斜槓,這裡不需要跟以前一樣新增)
第二個引數:該路由地址對應的檢視類
第三個引數:相當於是一個路由別名

來自官方文件:

register() 方法有兩個強制引數:

  • prefix - 用於此組路由的URL字首。
  • viewset - 處理請求的viewset類。

還可以指定一個附加引數(可選):

  • base_name - 用於建立的URL名稱的基本名稱。如果不設定該引數,將根據檢視集的queryset屬性(如果有)來自動生成基本名稱。
  • 注意,如果檢視集不包括queryset屬性,那麼在註冊檢視集時必須設定base_name

SimpleRouter會生成兩個介面:

image-20230206212551331

可以發現一個是 books/另一個是books/pk/

使用DefaultRouter

DefaultRouter比SimpleRouter多寫了一些介面:

image-20230206213522340

還包括一個預設返回所有列表檢視的超連結的API根檢視。
訪問根,可以看到有哪些地址:

【Django drf】檢視層大總結 ViewSetMixin原始碼分析 路由系統 action裝飾器

註冊路由的兩種方式

直接新增到urlpatterns列表

from rest_framework.routers import SimpleRouter, DefaultRouter

router = DefaultRouter()
router.register('books', views.BookView, 'books')
# router.register('api/v1/books', views.BookView, 'books') 

urlpatterns = [
   path('admin/', admin.site.urls),
]

urlpatterns += router.urls  # 在這裡新增

# router.urls也是一個列表:
[<URLPattern '^books/$' [name='books-list']>, <URLPattern '^books/(?P<pk>[^/.]+)/$' [name='books-detail']>]

使用路由分發include

from rest_framework.routers import SimpleRouter

router = SimpleRouter()
router.register('books', views.BookView, 'books')

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(router.urls))
]

自動生成路由底層實現

# 自動生成路由底層實現
       -本質是自動做對映,能夠自動生成的前提是,檢視類中要有 5個方法的某個或多個
           get--->list
           get---->retrieve
           put---->update
           post---->create
           delete---->destory

# 什麼時候可以自動生成路由?
       - ModelViewSet,ReadOnlyModelViewSet 可以自動生成
       - 9個檢視子類 + 配合ViewSetMixin   可以自動生成
       - GenericAPIView + 5個試圖擴充套件類+配合ViewSetMixin 可以自動生成

什麼時候可以自動生成路由?
前提: 有list,create...等方法 有ViewSetMixin魔法類

image-20230206112805201

自動生成路由使用的多,ModelViewSet用的不多,因為我們通常使用一個或者兩個介面 。
所以如下這個組合用的多:
9個試圖子類 + 配合ViewSetMixin
9個檢視子類提供listcreate...方法 ViewSetMixin反射進行物件屬性替換,使得get對應list 。

action裝飾器

使用裝飾器會將被裝飾的方法的名字新增在原路由的後面,生成一個新路由:
原路由:send/
裝飾器新增的路由:send/方法名/

# action 寫在檢視類的方法上,可以自動生成路由

# 使用步驟
	- 1 寫在檢視類方法上
    class SendView(ViewSet):
        # methods指定請求方法,可以傳多個
        # detail:只能傳True和False
        	-False,不帶id的路徑:send/send_sms/
            -True,帶id的路徑:send/2/send_sms/
        # url_path:生成send後路徑的名字,預設以方法名命名 
        # url_name:別名,反向解析使用,瞭解即可
        @action(methods=['POST'], detail=False)
        def send_sms(self, request):
            
            
 # 以後看到的drf路由寫法
	後期,都是自動生成,一般不在urlpatterns 加入路由了
    
 # 補充:
	-1 不同請求方式可以使用不同序列化類
    -2 不同action使用不同序列化類
class SendView(GenericViewSet):
    queryset = None
    serializer_class = '序列化類'

    def get_serializer(self, *args, **kwargs):
        if self.action=='lqz':
            return '某個序列化類'
        else:
            return '另一個序列化列'
    @action(methods=['GET'], detail=True)
    def send_sms(self, request,pk):
        print(pk)
        # 手機號,從哪去,假設get請求,攜帶了引數
        phone = request.query_params.get('phone')
        print('傳送成功,%s' % phone)
        return Response({'code': 100, 'msg': '傳送成功'})

    @action(methods=['GET'], detail=True)
    def lqz(self,request):  # get
        # 序列化類
        pass

    @action(methods=['GET'], detail=True)
    def login(self,request):  # get
        # 序列化類
        pass

無法自動生成的路由

我們知道路由自動生成,是實現了請求(get)和類中方法(list)的對應。
如果我們在類中寫listcreate,... ,updata之外的方法呢?
還能自動生成這些方法的路由嗎?

示例:

image-20230206113524026

get攜帶引數,引數是手機號

路由怎麼寫?

image-20230206113649353

如果這樣寫,那就相當於get請求對映send_sms方法而不是list方法。

自動生成路由,只能對映到list,create...,但是我們需要執行send_sms,並且區分開原來的list方法,所以需要加drf提供的裝飾器:
加上這個裝飾之後,會新增一個路由send/send_sms/

image-20230206114047602

這樣就可以對這個新增的路由傳送請求了。

不同action使用不同序列化類

class SendView(GenericViewSet):
    queryset = None
    serializer_class = '序列化類'

    def get_serializer(self, *args, **kwargs):
        if self.action=='lqz':
            return '某個序列化類'
        else:
            return '另一個序列化列'
    @action(methods=['GET'], detail=True)
    def send_sms(self, request,pk):
        print(pk)
        # 手機號,從哪去,假設get請求,攜帶了引數
        phone = request.query_params.get('phone')
        print('傳送成功,%s' % phone)
        return Response({'code': 100, 'msg': '傳送成功'})

    @action(methods=['GET'], detail=True)
    def lqz(self,request):  # get
        # 序列化類
        pass

    @action(methods=['GET'], detail=True)
    def login(self,request):  # get
        # 序列化類
        pass

如何實現不同的方法,使用不同的序列化類?

image-20230206115407915

用action產生的路徑來判斷不同的get請求。

image-20230206115446187

我怎麼知道self裡面有個action,在什麼時候放進去的?

在ViewSetMixin:

image-20230206115825972

自動生成路由時才會有action屬性.

image-20230206115841198

action_map是as_view傳入的字典。

檢視self.action:

image-20230206115951344

認證元件前戲

登入介面

# 訪問某個介面,需要登陸後才能訪問

# 第一步:寫個登入功能,使用者表
	-User表
    -UserToken表:儲存使用者登入狀態 [這個表可以沒有,如果沒有,把欄位直接寫在User表上也可以]

隨機字串可以放在user表,也可以放在usertoken表裡。

建表:
使用者刪掉掉了之後,使用者token沒有存在的必要。所以可以使用級聯刪除。

image-20230206122757085

雖然UserToken中沒有外來鍵,但是UserToken還是可以進行反向查詢,其生成的物件中有一個user屬性。(反向查詢表名小寫)

登入介面:
登入介面是不需要使用序列化類的。
使用uuid模組生成隨機字串。

關於傳給前端的隨機字串,最好不要用時間戳。
時間戳怎麼重複?不同機器可能出現同一時間戳,及不同機器同一時間登入。

image-20230206122842582

updata_or_create方法:
根據user去查,如果能查到,就把default裡面的token給放進去。也就是如果有token就更新,如果沒有就建立。

登入介面:

#### 表模型

class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)


class UserToken(models.Model):  # 跟User是一對一
    token = models.CharField(max_length=32)
    user = models.OneToOneField(to='User', on_delete=models.CASCADE, null=True)
    # user :反向,表名小寫,所有有user欄位

### 路由
router.register('user', views.UserView, 'user')  # /api/v1/user/login     post 請求

# 檢視類
####  登入介面  自動生成路由+由於登入功能,不用序列化,繼承ViewSet
from .models import User, UserToken
import uuid


class UserView(ViewSet):
    @action(methods=['POST'], detail=False)
    def login(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = User.objects.filter(username=username, password=password).first()
        if user:
            # 使用者存在,登入成功
            # 生成一個隨機字串--uuid
            token = str(uuid.uuid4())  # 生成一個永不重複的隨機字串
            # 在userToken表中儲存一下:1 從來沒有登入過,插入一條,     2 登入過,修改記錄
            # 如果有就修改,如果沒有就新增  (if 自己寫)
            # kwargs 傳入的東西查詢,能找到,使用defaults的更新,否則新增一條
            UserToken.objects.update_or_create(user=user, defaults={'token': token})
            return Response({'code': '100', 'msg': '登入成功', 'token': token})
        else:
            return Response({'code': '101', 'msg': '使用者名稱或密碼錯誤'})

相關文章