• django-rest-framework-源码解析003-视图家族和路由(APIView/GenericAPIView/mixins/generics/viewsets)


    视图家族

    视图家族在rest_framework源码位置和学习曲线为:

    rest_framework.views: 基本视图(APIView)

    rest_framework.generics: 工具视图(GenericAPIView)

    rest_framework.mixins: 视图工具集(Create/Destroy/List/Retrieve/Update)

    rest_framework.viewsets: 视图集

    APIView

    前几章代码都是基于APIView写的, 这里就不赘述了, 简单来说APIView做了三件事:

    1. 重写了as_view()方法, 在as_view()中主要调用了父类的as_view()方法 view = super().as_view(**initkwargs) , 并在最后返回时, 加上了csrf验证 return csrf_exempt(view) 

    2. 重写了dispatch()方法, 在父类的as_view()中被调用, 这是DRF的真正核心主函数, 在dispatch()中实现了各大模块

    GenericAPIView

    源码为: rest_framework.generics.GenericAPIView

    class GenericAPIView(views.APIView):
        """
        Base class for all other generic views.
        """
        # You'll need to either set these attributes,
        # or override `get_queryset()`/`get_serializer_class()`.
        # If you are overriding a view method, it is important that you call
        # `get_queryset()` instead of accessing the `queryset` property directly,
        # as `queryset` will get evaluated only once, and those results are cached
        # for all subsequent requests.
        queryset = None
        serializer_class = None
    
        # If you want to use object lookups other than pk, set 'lookup_field'.
        # For more complex lookup requirements override `get_object()`.
        lookup_field = 'pk'
        lookup_url_kwarg = None
    
        # The filter backend classes to use for queryset filtering
        filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
    
        # The style to use for queryset pagination.
        pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
    
        def get_queryset(self):
            .......

    1. 继承与APIView, 保留了APIView的特性, 并在其基础上定义了一下更加方便易用的属性

    2. 定义了很多类属性, 如queryset(ORM查找结果集), serializer_class(序列化类), lookup_field(主键字段变量名), 

    3. 对于上述属性定义了对应的get_xxx方法, 如get_queryset(获取结果查找集), get_serializer(获取序列化类), get_object(获取模型对象)等等

    如何使用GenericAPIView

    让我们先回顾一下APIView如何实现查询接口的

    class BookAPIView(APIView):
        def get(self, request, *args, **kwargs):
            pk = kwargs.get('pk')
            if pk:
                # 单查
                books = Book.objects.get(pk=pk)
                many = False
            else:
                # 群查
                books = Book.objects.filter(is_delete=False).all()
                many = True
            serializer = BookSerializer(instance=books, many=many)
            return MyResponse(result=serializer.data)

    可以看到一般查询接口分为3步:

    1. 获取结果查找集books

    2. 序列化查询结果集得到序列化对象serializer

    3. 返回响应

    GenericAPIView就是将这通用的三步, 进行封装, 让使用者只需要编写关键的变量值即可

    将上面的代码转化为GenericAPIView就是:

    class BookGenericAPIView(GenericAPIView):
        queryset = Book.objects.filter(is_delete=False)
        serializer_class = BookSerializer
    
        def get(self, request, *args, **kwargs):
            pk = kwargs.get('pk')
            if pk:
                # 单取
                return self.retrieve(request, *args, **kwargs)
            # 群取
            return self.list(request, *args, **kwargs)
    
        # 单取
        def retrieve(self, request, *args, **kwargs):
            # books = Book.objects.filter(is_delete=False, pk=pk).first()
            books = self.get_object()
            # serializer = BookSerializer(instance=books)
            serializer = self.get_serializer(instance=books)
            return MyResponse(result=serializer.data)
    
        # 群取
        def list(self, request, *args, **kwargs):
            # books = Book.objects.filter(is_delete=False).all()
            books = self.get_queryset()
            # serializer = BookSerializer(instance=books, many=True)
            serializer = self.get_serializer(instance=books, many=True)
            return MyResponse(result=serializer.data)

    这里将单取和群取做成了两个独立的函数, 且这样看似使用GenericAPIView时代码似乎比使用APIView更多, 但是先不要着急, 我们可以配合后面的mixin一起使用时就不需要写这么多代码了

    使用GenericAPIView时, 将原先使用APIView的写法进行了如下替换:

    1. 定义了两个变量, queryset用于保存查询结果集, serializer用于保存序列化类

    2. 将原获取查询结果集的ORM写法  books = Book.objects.filter(is_delete=False).all() 换成了 books = self.get_queryset() 或 books = self.get_object() , 这里需要注意的一点就是在get_object()内部, 默认是通过参数名'pk'获取具体的主键ID, 而这就需要在url中的参数名也要为'pk', 若url中参数名为'xxx', 则需要重写GenericAPIView中的lookup_field = 'xxx'

    3. 将原通过实例化序列化类写法  serializer = BookSerializer(instance=books) 换成了 serializer = self.get_serializer(instance=books) 

    其中get_queryset()和get_serializer()其实就是去拿到类属性queryset和Serializer_class, 然后再进行实例化操作, 即将原来APIView的写法拆成了两步: 第一步, 定义类属性; 第二步, 获取类属性并实例化.

    Mixins工具集

    上面手动简单实现了GenericAPIView的单查retrieve和群查list, 而mixins的工具集中就已经封装好了对应的函数, 且功能更加强大, 源码位置为: rest_framework.mixins.py

    mixins中主要定义了5个类: CreateModelMixin(用于创建模型类对象), DestroyModelMixin(用于删除模型类对象), ListModelMixin(用于群查模型类对象), RetrieveModelMixin(用于单查模型类对象), UpdateModelMixin(用于单增模型类对象)

    Mixins的使用

    我们先跟着上面的查询接口, 看一下ListModelMixin用法, 查看ListModelMixin源码可以看到, 其群查的list方法和我们上面手动写的list方法很像, 只是多加了过滤器和分页器的功能

    class ListModelMixin:
        """
        List a queryset.
        """
        def list(self, request, *args, **kwargs):
            # 过滤器
            queryset = self.filter_queryset(self.get_queryset())
    
            # 分页器
            page = self.paginate_queryset(queryset)
            if page is not None:
                serializer = self.get_serializer(page, many=True)
                return self.get_paginated_response(serializer.data)
    
            serializer = self.get_serializer(queryset, many=True)
            return Response(serializer.data)

    具体用法为:

    class BookMixinGenericAPIView(ListModelMixin, GenericAPIView):
        queryset = Book.objects.filter(is_delete=False)
        serializer_class = BookSerializer
    
        def get(self, request, *args, **kwargs):
            # 群查
            # mixins提供的list方法的响应对象是Response, 想将该对象格式化为自定的MyResponse
            # response的数据都存放在response.data中
            response = self.list(self, request, *args, **kwargs)
            return MyResponse(result=response.data)

    可以看到mixin工具类和GenericAPIView组合使用时, 实现群查接口非常方便, 只需要三步:

    1. 继承ListModelMixin和GenericAPIView

    2. 定义类属性queryset和Serializer_class

    3. 调用list方法并返回response

    同样想要实现单查的话, 再继承一下RetrieveModelMixin, 然后手动分配一下get的方法调用即可, 需要注意的是继承时, 先写mixins的类, 最后写GenericAPIView, 因为是在mixin中调用了GenericAPIView的方法

    class BookMixinGenericAPIView(mixins.ListModelMixin, mixins.RetrieveModelMixin,
                                  GenericAPIView):
        queryset = Book.objects.filter(is_delete=False)
        serializer_class = BookSerializer
    
        def get(self, request, *args, **kwargs):
            pk = kwargs.get('pk')
            if pk:
            # 单查
                response = self.retrieve(request, *args, **kwargs)
            else:
                # 群查
                response = self.list(self, request, *args, **kwargs)
            # mixins提供的list方法的响应对象是Response, 想将该对象格式化为自定的MyResponse
            # response的数据都存放在response.data
            return MyResponse(result=response.data)

     同理, mixins还提供了其他的单增/单删/单整体改/单局部改的接口, 完整案例如下:

    from rest_framework.generics import GenericAPIView
    from rest_framework import mixins
    from utils.response import MyResponse
    from books.models import Book
    from books.serializers import BookSerializer
    
    class BookMixinGenericAPIView(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
                                  mixins.DestroyModelMixin, mixins.UpdateModelMixin,
                                  GenericAPIView):
        queryset = Book.objects.filter(is_delete=False)
        serializer_class = BookSerializer
    
        #
        def get(self, request, *args, **kwargs):
            pk = kwargs.get('pk')
            if pk:
                # 单查
                response = self.retrieve(request, *args, **kwargs)
            else:
                # 群查
                response = self.list(self, request, *args, **kwargs)
            # mixins提供的list方法的响应对象是Response, 想将该对象格式化为自定的MyResponse
            # response的数据都存放在response.data
            return MyResponse(result=response.data)
    
        # 单增
        def post(self, request, *args, **kwargs):
            response = self.create(request, *args, **kwargs)
            return MyResponse(result=response.data)
    
        # 单删
        def delete(self, request, *args, **kwargs):
            response = self.destroy(request, *args, **kwargs)
            return MyResponse(result=response.data)
    
        # 单整体改
        def put(self, request, *args, **kwargs):
            response = self.update(request, *args, **kwargs)
            return MyResponse(result=response.data)
    
        # 单局部改
        def patch(self, request, *args, **kwargs):
            response = self.partial_update(request, *args, **kwargs)
            return MyResponse(result=response.data)

    mixins的钩子函数

    在上面我们调用了mixin的self.create/destroy/update方法实现增删改, 但如果我们对增删改操作还有一些附加的操作(比如在新增时触发发送邮件功能)或者不想用其默认的方法(比如它原来的删除确实是把数据直接删除掉了, 而我们想要的删除只是改一下数据的is_dalete状态)时, mixins给我们提供了相应的钩子函数perform_create/perform_destroy/perform_update, 这些函数都在源码相应的类中定义了:

    def perform_create(self, serializer):
        serializer.save()
    
    def perform_destroy(self, instance):
        instance.delete()
    
    def perform_update(self, serializer):
        serializer.save()

    现在我们在我们的视图类中重写一下删除的操作perform_destroy, 将数据的is_dalete状态改为True

        # 单删
        def perform_destroy(self, instance):
            instance.is_delete = True
            instance.save()
    
        def delete(self, request, *args, **kwargs):
            response = self.destroy(request, *args, **kwargs)
            return MyResponse(result=response.data)

    generics

    上面我们把GenericAPIView和mixins的工具集联合使用, 同时继承, 可以实现简单的增删改查接口, 在generics.py中, 除了提供了GenericAPIView外, 还提供了一些其他的类, 如 CreateAPIView/DestroyAPIView/ UpdateAPIView/ ListCreateAPIView/ RetrieveDestroyAPIView 等等, 这些类都是上我们上面同时继承mixin和GenericAPIView一样, 是帮我们封装好的一些工具类, 源码如下:

    # 单增
    class CreateAPIView(mixins.CreateModelMixin,
                        GenericAPIView):
        """
        Concrete view for creating a model instance.
        """
        def post(self, request, *args, **kwargs):
            return self.create(request, *args, **kwargs)
    
    # 群查
    class ListAPIView(mixins.ListModelMixin,
                      GenericAPIView):
        """
        Concrete view for listing a queryset.
        """
        def get(self, request, *args, **kwargs):
            return self.list(request, *args, **kwargs)
    
    # 单查
    class RetrieveAPIView(mixins.RetrieveModelMixin,
                          GenericAPIView):
        """
        Concrete view for retrieving a model instance.
        """
        def get(self, request, *args, **kwargs):
            return self.retrieve(request, *args, **kwargs)
    
    # 单删
    class DestroyAPIView(mixins.DestroyModelMixin,
                         GenericAPIView):
        """
        Concrete view for deleting a model instance.
        """
        def delete(self, request, *args, **kwargs):
            return self.destroy(request, *args, **kwargs)
    
    # 单改
    class UpdateAPIView(mixins.UpdateModelMixin,
                        GenericAPIView):
        """
        Concrete view for updating a model instance.
        """
        def put(self, request, *args, **kwargs):
            return self.update(request, *args, **kwargs)
    
        def patch(self, request, *args, **kwargs):
            return self.partial_update(request, *args, **kwargs)
    
    # 群查和单增
    class ListCreateAPIView(mixins.ListModelMixin,
                            mixins.CreateModelMixin,
                            GenericAPIView):
        """
        Concrete view for listing a queryset or creating a model instance.
        """
        def get(self, request, *args, **kwargs):
            return self.list(request, *args, **kwargs)
    
        def post(self, request, *args, **kwargs):
            return self.create(request, *args, **kwargs)
    
    # 单查和单改
    class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
                                mixins.UpdateModelMixin,
                                GenericAPIView):
        """
        Concrete view for retrieving, updating a model instance.
        """
        def get(self, request, *args, **kwargs):
            return self.retrieve(request, *args, **kwargs)
    
        def put(self, request, *args, **kwargs):
            return self.update(request, *args, **kwargs)
    
        def patch(self, request, *args, **kwargs):
            return self.partial_update(request, *args, **kwargs)
    
    # 单查和单删
    class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
                                 mixins.DestroyModelMixin,
                                 GenericAPIView):
        """
        Concrete view for retrieving or deleting a model instance.
        """
        def get(self, request, *args, **kwargs):
            return self.retrieve(request, *args, **kwargs)
    
        def delete(self, request, *args, **kwargs):
            return self.destroy(request, *args, **kwargs)
    
    # 单查和单改和单删
    class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
                                       mixins.UpdateModelMixin,
                                       mixins.DestroyModelMixin,
                                       GenericAPIView):
        """
        Concrete view for retrieving, updating or deleting a model instance.
        """
        def get(self, request, *args, **kwargs):
            return self.retrieve(request, *args, **kwargs)
    
        def put(self, request, *args, **kwargs):
            return self.update(request, *args, **kwargs)
    
        def patch(self, request, *args, **kwargs):
            return self.partial_update(request, *args, **kwargs)
    
        def delete(self, request, *args, **kwargs):
            return self.destroy(request, *args, **kwargs)

    viewsets

    在使用了上面的mixin工具后, 会发现增删改查的函数名(create/destroy/partial_update/update/list/retrieve)与对应的请求方式名(post/delete/patch/put/get)两者的命名并不是一样的, 而是存在一个对应关系或者映射关系.我们把两者结合的方式就是在视图类中通过dispatch的反射而定义与请求方式名一样的函数(post/delete/patch/put/get), 在函数中调用mixin的对应的方法.

    ViewSetMixin

    那么这种结合方式或者对应关系, 能不能写的更加简单一点呢? 在rest_framework.viewsets.py中, 有一个 ViewSetMixin 类, 这个类的作用就是重写了 as_view() 方法, 让其增加一个字典参数(actions), 把请求方式名与接口函数名的对应的关系直接写在字典参数(actions)中, 源代码分析为:

    class ViewSetMixin:
        """
        This is the magic.
    
        Overrides `.as_view()` so that it takes an `actions` keyword that performs
        the binding of HTTP methods to actions on the Resource.
    
        For example, to create a concrete view binding the 'GET' and 'POST' methods
        to the 'list' and 'create' actions...
    
        view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
        """
    
        @classonlymethod
        def as_view(cls, actions=None, **initkwargs):
            ....代码省略
    
            # actions must not be empty
            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)
                # We also store the mapping of request methods to actions,
                # so that we can later set the action attribute.
                # eg. `self.action = 'list'` on an incoming GET request.
                self.action_map = actions
    
                # Bind methods to actions
                # This is the bit that's different to a standard view
                # 绑定请求方式method与接口函数action的对应关系
                for method, action in actions.items():
                    handler = getattr(self, action)
                    setattr(self, method, handler)
                ....代码省略
                return self.dispatch(request, *args, **kwargs)
            ....代码省略
            view.actions = actions
            return csrf_exempt(view)

    可以看到:

    1. 在类的说明文档中, 就告诉了我们说这是重写的as_view()方法, 新增了一个actions参数用来绑定HTTP请求方式和对应的操作, 例如: MyViewSet.as_view({'get': 'list', 'post': 'create'})

    2. 在as_view中的view()方法中, 同样通过反射进行上述的绑定

    GenericViewSet/ViewSet

    上述的ViewSetMixin类只是重写了as_view()方法让其新增一个actions参数而已, 而具体使用时还需要和前面提到的GenericAPIView或者更基础的APIView一起使用, 因为具体的处理逻辑还是需要走dispatch方法.

    于是在viewsets.py中还提供了两个类, GenericViewSet(ViewSetMixin, generics.GenericAPIView) 和 ViewSet(ViewSetMixin, views.APIView) , 就像之前我们选择使用基础的APIView还是GenericAPIView一样, DRF也提供了两个对应的ViewSet类, 这里我们使用GenericViewSet实现简单的群查功能:

    # 在urls.py中添加配置
    from django.urls import path
    from books import views
    
    urlpatterns = [
        ...
        path('v4/', views.BookGenericViewSet.as_view({'get': 'list'})),
    ]
    
    # 在views.py中添加视图类
    from rest_framework.viewsets import GenericViewSet
    class BookGenericViewSet(GenericViewSet): queryset = Book.objects.filter(is_delete=False) serializer_class = BookSerializer def list(self, request): books = self.get_queryset() serializer = self.get_serializer(books, many=True) return MyResponse(result=serializer.data)

    ModelViewSet/ReadOnlyModelViewSet

    我们在上一步继承了GenericViewSet, 但是list方法还是我们自己手动写的, 我们可以结合上面的mixin工具一起使用, 例如同时继承mixins的ListModelMixin和GenericViewSet实现群查, 这样list方法在mixin中就定义好了, 我们也不需要手动写list方法, 视图类代码就变得更加简单, 只需要定义类属性queryset和Serializer_class即可:

    # 在urls.py中添加配置
    from django.urls import path
    from books import views
    
    urlpatterns = [
        ...
        path('v5/', views.BookListMixinGenericViewSet.as_view({'get': 'list'})),
    ]
    
    # 在views.py中添加视图类
    from rest_framework import mixins
    from rest_framework.viewsets import GenericViewSet
    
    class BookListMixinGenericViewSet(mixins.ListModelMixin, GenericViewSet):
        queryset = Book.objects.filter(is_delete=False)
        serializer_class = BookSerializer

    同理, 我们还可以加上其他接口的mixin工具, 如单查mixins.RetrieveModelMixin, 单增mixins.CreateModelMixin等等, 而这一步DRF也为我们想好了, 在viewsets中定义了一个 ModelViewSet 类, 它继承了mixin的所有接口类

    class ModelViewSet(mixins.CreateModelMixin,
                       mixins.RetrieveModelMixin,
                       mixins.UpdateModelMixin,
                       mixins.DestroyModelMixin,
                       mixins.ListModelMixin,
                       GenericViewSet):
        """
        A viewset that provides default `create()`, `retrieve()`, `update()`,
        `partial_update()`, `destroy()` and `list()` actions.
        """
        pass

    同时, DRF还定义了一个 ReadOnlyModelViewSet 类, 顾名思义, 这个类只能用来查询, 只继承了 单查mixins.RetrieveModelMixin和 群查 mixins.ListModelMixin两个接口工具类

    class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                               mixins.ListModelMixin,
                               GenericViewSet):
        """
        A viewset that provides default `list()` and `retrieve()` actions.
        """
        pass

    我们可以按需使用ModelViewSet类还是ReadOnlyModelViewSet, 这里我们使用ModelViewSet, 不需要几行代码就能完成简单的单增, 单删, 单整体改, 单局部改, 单查, 群查6个接口, 但是这里需要注意的是由于使用的是mixin的默认接口函数, 我们之前自定义的响应类MyResponse就无法使用了, 除非我们再重写一下这些默认的接口函数:

    # 在urls.py中添加配置
    from django.urls import path
    from books import views
    
    urlpatterns = [
        ...
        path('v6/', views.BookModelViewSet.as_view({'get': 'list', 'post': 'create', })),
        path('v6/<int:pk>/', views.BookModelViewSet.as_view(
            {'get': 'retrieve', 'delete': 'destroy', 'put': 'update', 'patch': 'partial_update'})),
    ]
    
    # 在views.py中添加视图类
    from rest_framework import mixins
    from rest_framework.viewsets import ModelViewSet
    
    class BookModelViewSet(ModelViewSet):
        queryset = Book.objects.filter(is_delete=False)
        serializer_class = BookSerializer

    路由(Router)

    有一些像Rails这样的Web框架提供自动生成Urls的功能。但是Django并没有。 REST framework为Django添加了这一功能,以一种简单、快速、一致的方式。

    DRF的routers模块没有什么东西,主要包含下面几个类:

    BaseRouter:路由的基类

    SimpleRouter: 继承了BaseRouter,常用类之一

    DefaultRouter:继承了SimpleRouter,常用类之一

    APIRootView

    我们主要使用的其实就是SimpleRouter和DefaultRouter, 基本用法为, 编辑urls.py:

    from rest_framework.routers import DefaultRouter
    
    # 1.创建路由对象
    router = DefaultRouter()
    # 2.注册视图类
    router.register(r'v6', views.BookModelViewSet, basename='v6')
    router.register(r'v5', views.BookListMixinGenericViewSet, basename='v5')
    
    urlpatterns = [
        # path('v5/', views.BookListMixinGenericViewSet.as_view({'get': 'list'})),
        # path('v6/', views.BookModelViewSet.as_view({'get': 'list', 'post': 'create', })),
        # path('v6/<int:pk>/', views.BookModelViewSet.as_view(
        #     {'get': 'retrieve', 'delete': 'destroy', 'put': 'update', 'patch': 'partial_update'})),
        
        # 3.将路由类的urls添加到urlpatterns中
        path('', include(router.urls)),
    ]
  • 相关阅读:
    LeetCode#237 Delete Node in a Linked List
    数据库概念
    请基于 TCP/IP 五层网络模型描述下当我们在浏览器请求 https ://pp.io 站点时,发生了哪些事情
    高并发大流量
    mysql关于索引
    php基础——会话控制
    php基础——运算符知识
    php基础——常量及数据类型考察
    引用变量和cow机制
    关系基本特性的运算封闭性
  • 原文地址:https://www.cnblogs.com/gcxblogs/p/13338991.html
Copyright © 2020-2023  润新知