# 兩個檢視基礎類別
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
以後可能只希望寫某幾個介面,而不是全部介面都存在,
可以通過繼承不同的檢視類實現。
只要查詢所有和刪除一個,怎麼寫?
範例:
為什麼沒有Destroy和Updata的組合?
因為必須先查出來,再修改或刪除,所以沒有這個組合。
之後會繼續封裝:兩個檢視類 ---> 一個檢視類
問題:
1.有兩個get請求對應一個CBV中get方法
2.兩個路由路徑對應一個CBV
# 路由
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內部繼承關係:
他的內部就是寫了get方法,我們get請求來了之後,就會呼叫這個方法,然後再去呼叫父類別的list方法。
ModelViewSet內部居然沒有寫,這是怎麼回事?
這是因為ModelViewSet繼承的最後一個類GenericViewSet,這是一個魔法類,他重寫了as_view。
我們直接發個請求執行一下。
會發現如下報錯:
可以得知,一旦繼承ModelViewSet,路由層的寫法就變了!
現在需要這樣寫:
這樣寫的意思是:
對於books/
這個路由:
get請求 --執行--> list方法
post請求 --執行--> create方法
對於books/<int:pk>/
這個路由:
get請求 --執行--> retrieve方法
put請求 --執行--> updata方法
delete請求 --執行--> destroy方法
先記住這個格式,知道怎麼用,後續原始碼分析再詳細瞭解。
# 路由
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內部繼承關係:
路由寫法為什麼變了?
導致路由寫法變了的原因是: ViewSetMixin
當請求來了之後,會執行ViewSetMixin類中的as_view方法的返回值。
# 請求來了,路由匹配成功---》get請求,匹配成功books,會執行 views.BookView.as_view({'get': 'list', 'post': 'create'})()------>讀as_view【這個as_view是ViewSetMixin的as_view】
從路由層開始分析,根據繼承屬性一個一個找as_view方法(從左往右)
ListModelMixin
,RetrieveModelMixin
,CreateModelMixin
,UpdateModelMixin
,DestroyModelMixin
這些方法中都沒有as_view。
所以會進入到GenericViewSet:
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 以後檢視類類中的方法名,可以任意命名,只要在路由中做好對映即可【重要】
實際上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下有這幾個類:
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):
在檢視類寫的方法可以任意命名,只要在路由層的字典寫好對映關係就行。
只要想變路由,就要繼承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 -->生成的路由更多
第二步:範例化得到物件。
第三步:註冊路由。路徑和檢視類建立關係 有幾個檢視類就要寫幾次
第四步:在urlpatterns註冊
也就是將生成好的路由,新增到urlpatterns列表。
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會生成兩個介面:
可以發現一個是 books/
另一個是books/pk/
。
DefaultRouter比SimpleRouter多寫了一些介面:
還包括一個預設返回所有列表檢視的超連結的API根檢視。
存取根,可以看到有哪些地址:
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']>]
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魔法類
自動生成路由使用的多,ModelViewSet用的不多,因為我們通常使用一個或者兩個介面 。
所以如下這個組合用的多:
9個試圖子類 + 配合ViewSetMixin
9個檢視子類提供list
,create
...方法 ViewSetMixin反射進行物件屬性替換,使得get對應list 。
使用裝飾器會將被裝飾的方法的名字新增在原路由的後面,生成一個新路由:
原路由: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)的對應。
如果我們在類中寫list
,create
,... ,updata
之外的方法呢?
還能自動生成這些方法的路由嗎?
範例:
get攜帶引數,引數是手機號
路由怎麼寫?
如果這樣寫,那就相當於get請求對映send_sms方法而不是list方法。
自動生成路由,只能對映到list,create...,但是我們需要執行send_sms,並且區分開原來的list方法,所以需要加drf提供的裝飾器:
加上這個裝飾之後,會新增一個路由send/send_sms/
。
這樣就可以對這個新增的路由傳送請求了。
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
如何實現不同的方法,使用不同的序列化類?
用action產生的路徑來判斷不同的get請求。
我怎麼知道self裡面有個action,在什麼時候放進去的?
在ViewSetMixin:
自動生成路由時才會有action屬性.
action_map是as_view傳入的字典。
檢視self.action:
# 存取某個介面,需要登陸後才能存取
# 第一步:寫個登入功能,使用者表
-User表
-UserToken表:儲存使用者登入狀態 [這個表可以沒有,如果沒有,把欄位直接寫在User表上也可以]
隨機字串可以放在user表,也可以放在usertoken表裡。
建表:
使用者刪掉掉了之後,使用者token沒有存在的必要。所以可以使用級聯刪除。
雖然UserToken中沒有外來鍵,但是UserToken還是可以進行反向查詢,其生成的物件中有一個user屬性
。(反向查詢表名小寫)
登入介面:
登入介面是不需要使用序列化類的。
使用uuid模組生成隨機字串。
關於傳給前端的隨機字串,最好不要用時間戳。
時間戳怎麼重複?不同機器可能出現同一時間戳,及不同機器同一時間登入。
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': '使用者名稱或密碼錯誤'})