DRF 之view

DRF view

View

DRF中的view分成三个等级,最基本的APIView, 到GenericAPIView,再到GenericViewSet.

DRF 之view

Django用“视图”这个概念封装处理用户请求并返回响应的逻辑。视图是一个可调用对象,它不仅可以是基于函数,也可以是基于类的。函数是通过判断request.method来区分不同的请求,而基于类的视图则对它进行了封装,只需要实现对应的方法即可,如get,post. 不管django还是restframework中的视图类都是基于这一点进行的封装。

APIView

APIView是drf中所有view的父类,本身继承于Django的View. 。最直接封装的是对request,response都进行了封装,response里面做了一些认证,权限,限流之类处理。而response返回的结果是经过系列化的json.

与django中的view类似的是,APIView中只需要实现对应的方法如 get, post等。

class ArticleView(APIView):
    def get(self, request, *args, **kwargs):
        # 取数据
        pass
    def post(self, request, *args, **kwargs):
        # 增加数据
        pass
    def put(self, request, *args, **kwargs):
        # 全局更新
        pass
    def patch(self, request, *args, **kwargs):
        # 局部更新
        pass
    def delete(self, request, *args, **kwargs):
        # 删除
        pass

GenricAPIView

这里都是通用的APIView,所谓通用就是常用的增删改查,也就是restframework已经帮你封装好了。比如django的GenericView封装了ListView, DetailView,CreateView, UpdateView, DeleteView等通用视图类。drf中则封装得更多。

下面 分别看一下源码:

CreateAPIView

实现了post方法

class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

ListAPIView

实现get方法

class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    """
    Concrete view for listing a queryset.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

RetrieveAPIView

实现get方法,它与ListAPIView的不同是它获取单个对象,类似于django中的DetailView

class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    """
    Concrete view for retrieving a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

DestroyAPIView

实现delete方法

class DestroyAPIView(mixins.DestroyModelMixin,
                     GenericAPIView):
    """
    Concrete view for deleting a model instance.
    """
    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

UpdateAPIView

实现了put,patch两个方法,分别对应全局和局部更新。

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)

下面则是一些组合的视图类

ListCreateAPIView

既实现了get方法,又实现了post方法,即可以获取所有对象,也可以添加对象。

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)

RetrieveUpdateAPIView

实现了get, put, patch三个方法,这里的get是获取单个对象,put,patch则对应的更新

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

RetrieveDestroyAPIView

实现了get,delete方法,能获取,删除单个对象。

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)

RetrieveUpdateDestroyAPIView

这个是实现方法最多的视图类,get(获取单个对象),put,patch更新单个对象,delete则是删除单个对象,也就是这个类实现了对单个对象的查改删三种操作

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)

从上面这些通用视图类的继承关系我们可以看到,它们都是通过继承GenericAPIView,再加上其它的Mixin来实现的。事实上GenericAPIView中并没有实现增删改查中的任何一种操作,这些操作都是通过Mixin来完成的。

Mixins

知道了这些,当我们需要对通用视图类的行为作定制时,只需要对这些Mixin实现的方法进行重写即可。这一点后面再说,我们先看看restframework实现了哪些Mixin,是不是和上面的视图类能对应上呢?

DRF 之view

CreateModelMixin

CreateModelMixin中实现了三个方法,其中与增加对应的是前两个,create,perform_create.也就是说当我们继承CreateAPIView时,实际上是通过这两个方法来完成增加操作的。

class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

ListModelMixin

只实现了一个list方法,即ListAPIView通过它来返回对象列表。

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)

RetrieveModelMixin

实现了retrieve方法,获取单个对象。

class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

UpdateModelMixin

实现了update,perform_update,partial_update方法,update,partial_update分别对应的全局与局部更新,在这里如果我们需要对保存的数据作一定的定制可以重写perform_update方法。

class UpdateModelMixin:
    """
    Update a model instance.
    """
    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

DestroyModelMixin

最后一个Mixin是DestoryModelMixin,实现了destory,perform_destory方法,即删除。

class DestroyModelMixin:
    """
    Destroy a model instance.
    """
    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_destroy(self, instance):
        instance.delete()

ListAPIView应用举例:

很显然,Mixin的个数并没有上面的View那么多,这是因为View可能对Mixin进行组合来达到实现不同功能的目的。那我们在写自己的视图类时是不是也可以随意组合通用视图类呢?如果仔细看一下上面那些通用视图类中实现的方法,就会发现也有些是不能组合到一起的,比如ListAPIView,RetrieveAPIView,它们都实现了get方法,使用时就会出问题。

前面说到“当我们需要对通用视图类的行为作定制时,只需要对这些Mixin实现的方法进行重写即可”指的就是这些Mixin中的方法,比如在使用ListAPIView时我们要获取 额外的信息,就可以重写list方法:

这里拿最近写的一个订单视图类举例,我重写list方法,将订单信息进行了数据结构的重组,返回一个嵌套的字典

class Order(ListAPIView):
    authentication_classes = [UserAuthentication, ]
    serializer_class = ser_order.OrderModelSerializer
    queryset = models.Order.objects.all().order_by('-id')
    # filter_backends = [ser_order.OrderFilterBackends, ]

    def list(self, *args, **kwargs):
        response = super(Order, self).list(*args, **kwargs)
        if response.status_code != status.HTTP_200_OK:
            return response

        order_dict = OrderedDict()

        # 将订单的状态码,状态描述文字组成字典
        for item in models.Order.status_choices:
            order_dict[item[0]] = {'text': item[1], 'child': []}

        # 将每个状态对应的订单信息放到child列表中,即根据状态码进行了分类
        for row in response.data:
            order_dict[row['status']]['child'].append(row)

        response.data = order_dict
        return response

ViewSet

ViewSet其实是对前面内容的更高层的封装,但我们可以看到在ViewSet类中并没有实现任何特殊的内容,它只是继承了两个类ViewSetMixin, APIView.

DRF 之view

ViewSetMixin

ViewSetMixin是viewset的基础,由于代码太多我们就只看看它的最重要的方法:as_view()它将请求方法与对应的Mixin中的方法关联起来,比如‘get' -->'list', 'post'-->'create', 'put'-->'update'

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):
        """
        Because of the way class based views create a closure around the
        instantiated view, we need to totally reimplement `.as_view`,
        and slightly modify the view function that is created and returned.
        """
        # The name and description initkwargs may be explicitly overridden for
        # certain route configurations. eg, names of extra actions.
        cls.name = None
        cls.description = None

        # The suffix initkwarg is reserved for displaying the viewset type.
        # This initkwarg should have no effect if the name is provided.
        # eg. 'List' or 'Instance'.
        cls.suffix = None

        # The detail initkwarg is reserved for introspecting the viewset type.
        cls.detail = None

        # Setting a basename allows a view to reverse its action urls. This
        # value is provided by the router through the initkwargs.
        cls.basename = None

        # 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'})`")

        # sanitize keyword arguments
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r" % (
                    cls.__name__, key))

        # name and suffix are mutually exclusive
        if 'name' in initkwargs and 'suffix' in initkwargs:
            raise TypeError("%s() received both `name` and `suffix`, which are "
                            "mutually exclusive arguments." % (cls.__name__))

        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
            for method, action in actions.items():
                handler = getattr(self, action)
                setattr(self, method, handler)

            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get

            self.request = request
            self.args = args
            self.kwargs = kwargs

            # And continue as usual
            return self.dispatch(request, *args, **kwargs)

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())

        # We need to set these on the view function, so that breadcrumb
        # generation can pick out these bits of information from a
        # resolved URL.
        view.cls = cls
        view.initkwargs = initkwargs
        view.actions = actions
        return csrf_exempt(view)

    def initialize_request(self, request, *args, **kwargs):
        """
        Set the `.action` attribute on the view, depending on the request method.
        """
        request = super().initialize_request(request, *args, **kwargs)
        method = request.method.lower()
        if method == 'options':
            # This is a special case as we always provide handling for the
            # options method in the base `View` class.
            # Unlike the other explicitly defined actions, 'metadata' is implicit.
            self.action = 'metadata'
        else:
            self.action = self.action_map.get(method)
        return request

    def reverse_action(self, url_name, *args, **kwargs):
        """
        Reverse the action for the given `url_name`.
        """
        url_name = '%s-%s' % (self.basename, url_name)
        kwargs.setdefault('request', self.request)

        return reverse(url_name, *args, **kwargs)

    @classmethod
    def get_extra_actions(cls):
        """
        Get the methods that are marked as an extra ViewSet `@action`.
        """
        return [method for _, method in getmembers(cls, _is_extra_action)]

    def get_extra_action_url_map(self):
        """
        Build a map of {names: urls} for the extra actions.

        This method will noop if `detail` was not provided as a view initkwarg.
        """
        action_urls = OrderedDict()

        # exit early if `detail` has not been provided
        if self.detail is None:
            return action_urls

        # filter for the relevant extra actions
        actions = [
            action for action in self.get_extra_actions()
            if action.detail == self.detail
        ]

        for action in actions:
            try:
                url_name = '%s-%s' % (self.basename, action.url_name)
                url = reverse(url_name, self.args, self.kwargs, request=self.request)
                view = self.__class__(**action.kwargs)
                action_urls[view.get_view_name()] = url
            except NoReverseMatch:
                pass  # URL requires additional arguments, ignore

        return action_url

ViewSet

class ViewSet(ViewSetMixin, views.APIView):
    """
    The base ViewSet class does not provide any actions by default.
    """
    pass

GenericViewSet

GenericViewSet不提供任何方法,但由于它继承了GenericAPIView,所以也具体了如get_object,get_queryset,get_serializer等方法。另外它还继承 了ViewSetMixin,所以也具体了对应的方法如view方法等。

class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
    """
    The GenericViewSet class does not provide any actions by default,
    but does include the base set of generic view behavior, such as
    the `get_object` and `get_queryset` methods.
    """
    pass

ReadOnlyModelViewSet

ReadOnlyModelViewSet 主要达到read only的作用,即它不提供增删改的功能,而只获取对象,因为它只实现了list, retrieve功能。

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

ModelViewSet

ModelViewSet继承GenericViewSet,及一个Mixin. 但GenericViewSet又继承了ViewSetMixin,GenericAPIView,这样看ModelViewSet继承了所有的Mixin和通用视图类的父类。

class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
    """
    The GenericViewSet class does not provide any actions by default,
    but does include the base set of generic view behavior, such as
    the `get_object` and `get_queryset` methods.
    """
    pass


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

那么ViewSet到底高级在哪呢?

ViewSet应用举例:

class UserListViewSet(GenericViewSet, ListModelMixin):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer

如果我们使用上面的视图类:那么在as_view中我们就该指定get 与list的关系:

 UserListViewSet.as_view({'get': 'list'})

但通常有更简单的用法:先看代码:

from rest_framework import viewsets
from rest_framework import serializers


class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.User
        fields = ('url', 'username', 'email', 'groups')


class GroupSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Group
        fields = ('url', 'name')

class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer


class GroupViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows groups to be viewed or edited.
    """
    queryset = Group.objects.all()
    serializer_class = GroupSerializer

views.py
from django.conf.urls import url, include
from rest_framework import routers
from auth import views

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
    url(r'^', include(router.urls)),
]

VIewSet常常配合router使用,router可以自动将常用的 get绑定list,post绑定create这些操作完成,而不需要你在as_view中指定对应的关系了,连这个都省去了。

上一篇:ⅩⅣ:


下一篇:pytest运行时记录操作步骤的简单实现