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//', 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//', views.BookModelViewSet.as_view(
    #     {'get': 'retrieve', 'delete': 'destroy', 'put': 'update', 'patch': 'partial_update'})),
    
    # 3.将路由类的urls添加到urlpatterns中
    path('', include(router.urls)),
]

 

你可能感兴趣的:(django-rest-framework-源码解析003-视图家族和路由(APIView/GenericAPIView/mixins/generics/viewsets))