视图类代码优化
一、 优化get、post等方法
查看我们的代码,不难发现,我们实现的get、post、put等方法,不管对那个接口进行操作,代码都是一样的。那么我们就可以把这些方法抽取出来。
class ListModelMixin:
def list(self, request):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(instance=page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(instance=queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
# 此处需注意继承顺序,自定义的类在前面,GenericAPIView在后面
class ProjectViews(ListModelMixin, GenericAPIView):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['^name', '=leader']
ordering_fields = ['id', 'name', 'leader']
pagination_class = PageNumberPagination
def get(self, request: Request):
'''# 把这部分全部抽取出来写一个类,当前类继承抽取出来的类,调用父类的该方法就可以实现了
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(instance=page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(instance=queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
'''
return self.list(request)
1.1 mixins
其实上面提取的那些方法,在DRF框架中已经给我们提供了,只需要导入就可以使用了。上面的代码可以优化为:
from rest_framework import mixins
class ProjectViews(mixins.ListModelMixin, GenericAPIView):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['^name', '=leader']
ordering_fields = ['id', 'name', 'leader']
pagination_class = PageNumberPagination
def get(self, request: Request):
return self.list(request)
二、优化视图类继承关系
经过上面的优化后类视图变为:
class ProjectViews(mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericAPIView):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['^name', '=leader']
ordering_fields = ['id', 'name', 'leader']
pagination_class = PageNumberPagination
def get(self, request: Request):
return self.list(request)
def post(self, request: Request):
return self.create(request)
这样看虽然优化了很多,但是继承的类还是有些多,而且get方法和post方法的逻辑在每一个类中都是一样的,我们可以创建一个基类,就免了这许多麻烦。
class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericAPIView):
def get(self, request: Request):
return self.list(request)
def post(self, request: Request):
return self.create(request)
class ProjectViews(ListCreateAPIView):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['^name', '=leader']
ordering_fields = ['id', 'name', 'leader']
pagination_class = PageNumberPagination
drf框架提供了类似上述的APIview视图。路径为rest_framework.generics。我们可以根据项目的情况来选择需要的视图。
这样,上面的代码可以优化成这样:
from rest_framework import generics
class ProjectViews(generics.ListCreateAPIView):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['^name', '=leader']
ordering_fields = ['id', 'name', 'leader']
pagination_class = PageNumberPagination
此时,我们有两个类视图,一个是没有传id的,一个是传了id的,但是两个类视图中的queryset和serializer_class都是一样的,那么我们有没有什么办法只写一个类视图呢?
三、优化为一个类视图
首先把所有的方法提取出来,虽然两个get方法,但是他们调用的不是同一个方法。
该方法继承视图集(ViewSet),只有继承了视图集才支持请求方法名称与action名称一一对应的功能。
class ProjectViewSet(viewsets.ViewSet):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
def list(self, request, *args, **kwargs):
pass
def retrieve(self, request, *args, **kwargs):
pass
def update(self, request, *args, **kwargs):
pass
def destroy(self, request, *args, **kwargs):
pass
def partial_update(self, request, *args, **kwargs):
pass
但是Django只认那些固定的方法名称,例如get,post等。所以我们需要路由文件(url.py)使它支持我们上面的那些方法名。
- 只要继承了ViewSet的类视图,那么就支持请求方法名称与action名称进行一一对应的功能,这是ViewSetMixin提供的功能。
- 在as_view方法中添加字典,key为请求方法名称(get、post、put、delete、patch、option、head),value为需要调用的action方法名称
urlpatterns = [
path('projects/', views.ProjectViewSet.as_view({
'get': 'list',
'post': 'create'
})),
path('projects/<int:pk>/', views.ProjectViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})),
]
但是上面的视图类只继承了ViewSet,不支持get_queryset,get_serializer等方法,这些方法是GenericAPIView提供的。
视图集中提供了一个GenericViewSet,即继承了ViewSetMixin,又继承了generics.GenericAPIView。我们继承GenericViewSet就解决了问题。
class ProjectViewSet(viewsets.GenericViewSet):
...
我们视图类中的list、retrieve、update、destroy、partial_update方法,在mixins.RetrieveModelMixin,mixins.ListModelMixin,mixins.UpdateModelMixin,mixins.DestroyModelMixin中都有提供,我们只要继承这些类就不用写这些方法了,简化后的代码如下:
class ProjectViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
查看视图集的源代码,我们发现有如下的方法,非常符合我们的需求:
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
pass
于是再次修改我们的视图类:
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Projects.objects.all()
serializer_class = serializers.ProjectModelSerializer
至此,我们视图类的代码就精简为3行了,非常的方便。
当然,并不是所有的视图类都是继承ModelViewSet,我们还需要根据自己项目的实际情况,选择更适合的视图集去继承。
四、优化路由
如果我们有很多action,就会使得路由文件写起来很麻烦,我们可以使用router来自动生成路由。
修改url.py文件代码。
# 创建一个路由对象
# 方式一:
router = routers.SimpleRouter()
# 方式二:(会自动添加跟路由,作为获取当前数据的入口)
# router = routers.DefaultRouter()
# 注册路由
# 第一个参数为路由前缀(等同于path中的'projects/'),第二个参数为视图集
# 使用视图集中的路由机制,只会为特定的action(retrieve、update、list、create等)自动生成路由条件
# 默认自定义的action,不会自动生成路由条目,需要手动添加路由映射
router.register(r'projects', views.ProjectViewSet)
urlpatterns = [
# path('projects/', views.ProjectViewSet.as_view({
# 'get': 'list',
# 'post': 'create'
# })),
# path('projects/<int:pk>/', views.ProjectViewSet.as_view({
# 'get': 'retrieve',
# 'put': 'update',
# 'patch': 'partial_update',
# 'delete': 'destroy'
# })),
# 合并路径条目,方式二(一般不使用这种方法):
# path('', include(router.urls))
]
# 合并路径条目,方式一:
urlpatterns += router.urls
如果我们的视图中有自定义的action如下:
class ProjectViewSet(viewsets.ModelViewSet):
...
def names(self, request):
queryset = self.get_queryset()
names_list = [{
'id': project.id, 'name': project.name} for project in queryset]
return Response(names_list, status=status.HTTP_200_OK)
想要访问这个接口,我们就只能手动添加路由。
...
urlpatterns = [
path('projects/names/', views.ProjectViewSet.as_view({
'get': 'names',
})),
]
...
如果自定义的action有很多,这种方法就非常麻烦,那么我们需要有自动添加自定义路由的方法,修改视图文件,给自定义的action前加action装饰器即可:
from rest_framework.decorators import action
class ProjectViewSet(viewsets.ModelViewSet):
...
# @action(methods=['get'], detail=False, url_path='pp', url_name='nn')
@action(methods=['get'], detail=False)
def names(self, request):
queryset = self.get_queryset()
names_list = [{
'id': project.id, 'name': project.name} for project in queryset]
return Response(names_list, status=status.HTTP_200_OK)
action装饰器参数说明:
- methods参数默认为get方法,可以在列表中指定多个请求方法
- detail指定是否需要接收模型主键值,如果无需接收主键值,那么需要设置detail=False,否则需要设置detail=True
- url_path指定生成的路由条目路径名,默认为自定义action方法名称
- url_name指定生成的路由名称后缀,默认为自定义action方法名称
五、其他方法的优化
我们上面的name方法(获取所有项目的名称)能不能再优化一下呢,不难发现我们name方法没有使用序列化器类,在数据非常多的时候这样是很不方便的。
之前的序列化器类很明显不符合我们这个需求,我们需要再写一个序列化器类来匹配这个。
class ProjectNamesModelSerializer(serializers.ModelSerializer):
class Meta:
model = Projects
fields = ('id', 'name')
修改name方法:
@action(methods=['get'], detail=False)
def names(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = serializers.ProjectNamesModelSerializer(instance=queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
5.1 一个视图类使用多个序列化器类
但是这样把序列化器类写死不方便管理。查看源码我们发现,在调用get_serializer()时,会调用get_serializer_class()方法。我们可以通过重写get_serializer_class()方法来解决在一个视图类中出现多个序列化器类的问题。
@action(methods=['get'], detail=False)
def names(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(instance=queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def get_serializer_class(self):
if self.action == 'names':
return serializers.ProjectNamesModelSerializer
else:
return self.serializer_class
5.2 使用父类的方法返回
观察上面name方法中的代码,发现代码与mixins中的list()相似度极高,只是list多了分页操作而已。那么我们就可以用list来返回。
@action(methods=['get'], detail=False)
def names(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
但是此时我们多了分页功能,与需求不符。
5.3 跳过父类的过滤功能
父类有过滤功能,但是我们又不想使用过滤功能,我们可以通过重写filter_queryset()方法来跳过过滤功能。
def filter_queryset(self, queryset):
if self.action == 'names':
return self.queryset
else:
return super().filter_queryset(queryset)
5.4 跳过父类的分页功能
父类有分页功能,但是我们又不想使用分页功能,我们可以通过重写paginate_queryset()方法来跳过分页功能。
def paginate_queryset(self, queryset):
if self.action == 'names':
return None
else:
return super().paginate_queryset(queryset)
5.5 使用不同的查询集
我们可以通过重写get_queryset()方法来使用不同的查询集。
def get_queryset(self):
if self.action == 'names':
return self.queryset.filter(name__contains='Aaron')
else:
return super().get_queryset()
5.6 重写mixins下的几个方法
如果list、retrieve、update、destroy这些方法都不满足我们的需求,但是代码又差不多 我们也可以重写这些方法来达到我们的要求。
def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs)
project_id = response.data.pop('id')
return response
六、类视图设计原则
类视图的设计原则:
- 类视图尽量简化
- 根据需求选择相应的父类视图
- 如果DRF中的类视图有提供相应的逻辑,那么直接使用父类提供的
- 如果DRF中的类视图,绝大多数需要都能满足,那么直接重写父类的实现
- 如果DRF中的类视图完全不满足要求,直接自定义