【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
以后可能只希望写某几个接口,而不是全部接口都存在,
可以通过继承不同的视图类实现。
只要查询所有和删除一个,怎么写?
示例:
为什么没有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内部继承关系:
从注释也可以看出来他继承了 5个视图扩展类,也就是说ModelViewSet内部具备所有的"动作",也就是例如:
create()
、list()
、update()
、retrieve()
、destroy()
这些方法
但是我们请求来了,还是会调用视图类中的get()
、post()
、put()
、delete()
这些方法呀。
比如一个扩展子类:ListAPIView
他的内部就是写了get方法,我们get请求来了之后,就会调用这个方法,然后再去调用父类的list方法。
ModelViewSet内部居然没有写,这是怎么回事?
这是因为ModelViewSet继承的最后一个类GenericViewSet,这是一个魔法类,他重写了as_view。
我们直接发个请求运行一下。
会发现如下报错:
可以得知,一旦继承ModelViewSet,路由层的写法就变了!
现在需要这样写:
这样写的意思是:
-
对于
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内部继承关系:
这个类中只有list方法和retrieve方法。他同样继承了魔法类。
所以继承这个类就只能写两个只读接口:查询所有、查询一个
ViewSetMixin源码分析
查看GenericViewSet继承关系:
ViewSetMixin是个魔法类,重写了as_view:
查找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方法(从左往右)
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 以后视图类类中的方法名,可以任意命名,只要在路由中做好映射即可【重要】
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):
任意命名视图类的方法
在视图类写的方法可以任意命名,只要在路由层的字典写好映射关系就行。
只要想变路由,就要继承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列表。
使用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会生成两个接口:
可以发现一个是 books/
另一个是books/pk/
。
使用DefaultRouter
DefaultRouter比SimpleRouter多写了一些接口:
还包括一个默认返回所有列表视图的超链接的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魔法类
自动生成路由使用的多,ModelViewSet用的不多,因为我们通常使用一个或者两个接口 。
所以如下这个组合用的多:
9个试图子类 + 配合ViewSetMixin
9个视图子类提供list
,create
...方法 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)的对应。
如果我们在类中写list
,create
,... ,updata
之外的方法呢?
还能自动生成这些方法的路由吗?
示例:
get携带参数,参数是手机号
路由怎么写?
如果这样写,那就相当于get请求映射send_sms方法而不是list方法。
自动生成路由,只能映射到list,create...,但是我们需要执行send_sms,并且区分开原来的list方法,所以需要加drf提供的装饰器:
加上这个装饰之后,会新增一个路由send/send_sms/
。
这样就可以对这个新增的路由发送请求了。
不同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
如何实现不同的方法,使用不同的序列化类?
用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': '用户名或密码错误'})