【Django drf】视图层大总结 ViewSetMixin源码分析 路由系统 action装饰器

九个视图子类

# 两个视图基类 
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根视图。
访问根,可以看到有哪些地址:

注册路由的两种方式

直接添加到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': '用户名或密码错误'})
posted @ 2023-02-07 08:27  passion2021  阅读(249)  评论(0编辑  收藏  举报