Django REST framework(简称DRF)为我们提供强大的通用view的功能,本博客对这些view进行简要的总结分析。首先,我们看一下主要的几种view以及他们之间的关系。
这其中,还涉及了mixins,主要也分为5类:
mixins | 作用 | 对应HTTP的请求方法 |
mixins.ListModelMixin | 定义list方法,返回一个queryset的列表 | GET |
mixins.CreateModelMixin | 定义create方法,创建一个实例 | POST |
mixins.RetrieveModelMixin | 定义retrieve方法,返回一个具体的实例 | GET |
mixins.UpdateModelMixin | 定义update方法,对某个实例进行更新 | PUT/PATCH |
mixins.DestroyModelMixin | 定义delete方法,删除某个实例 | DELETE |
下面我们以主机(VirtualHost)作为一个例子,对view进行一个总结。下面是关于主机的一个Model。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# ./models.py from django.db import models from django.utils import timezone class VirtualHost(models.Model): hostname = models.CharField(verbose_name='Hostname', max_length=30,blank=True, null=True) ip = models.GenericIPAddressField(verbose_name='IP',blank=True, null=True) cpu = models.CharField(verbose_name='Cpu', max_length=30, blank=True, null=True) memory = models.CharField(verbose_name='Memory', max_length=30, blank=True, null=True) date = models.DateTimeField(default=timezone.now) owner = models.ForeignKey('auth.User', related_name='virtualhost', on_delete=models.CASCADE) class Meta: db_table = 'host' |
1. django View
首先,我们使用Django自带的view,也是最底层的view,获取一个课程的列表。DRF是通过json的格式进行数据交互的,所以这里也返回json数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# ./views.py import json from django.views.generic.base import View from django.http import HttpResponse from .models import VirtualHost class VirtualHostView(View): def get(self, request): """ 通过django的view实现课程列表页 :param request: :return: """ json_list = [] data = VirtualHost.objects.all()[:10] for host in data: json_dict = {} json_dict['hostnmae'] = host.hostname json_dict['ip'] = host.ip json_dict['cpu'] = host.cpu json_list.append(json_dict) return HttpResponse(json.dumps(json_list), content_type="application/json") |
可以看出,这里我们手动构建了一个字典。并且通过json.dumps反序列化成json格式,然后通过原生Django HttpResponse方法返回数据。这里使用HttpResponse方法返回json数据时,需要指定content_type为“application/json”,告诉浏览器返回的数据是json格式。
当然,手动构建字典这里也是可以简化一下的,直接使用Django models提供的model_to_dict方法来序列化字典。
1 2 3 4 |
from django.forms.models import model_to_dict for host in data: json_dict = model_to_dict(host) json_list.append(json_dict) |
此时,访问API应该会得到错误:Object of type ‘datetime’ is not JSON serializable。没错,我们手动构建的序列化方式没办法处理datetime、imagefieldfile及文件类型。只能处理一些字符及数值类型。那么我们就面临一个问题了,如何才能把任意类型都序列化呢?
其实,Django本身给我们提供了另外一个方法serializers,专门用来做序列化的。所以,我们可以利用这个方法来简化一下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# ./views.py import json from django.views.generic.base import View from django.core import serializers from django.http import HttpResponse, JsonResponse from .models import VirtualHost class VirtualHostView(View): def get(self, request): data = VirtualHost.objects.all()[:10] json_data = serializers.serialize('json', data) json_data = json.loads(json_data) return JsonResponse(json_data, safe=False) |
上面的代码我们使用了 Django serializers.serialize&JsonResponse 方法简化了一些工作,现在就可以处理任意类型的字段了,且代码看起来更加简洁。这样是不是就完美了呢?显然不是,目前它还只是能返回Json数据。比如以下一些更加复杂的工作:
- 由于 Django serializers.serialize 是固化的,就是说帮我们返回了 Model 中所有字段,没办法自定义返回的。
- 默认 serializers.serialize 返回三个字段:model、pk和fields;其中 Model 字段都包含在了它自身的“fields”字段下,所以我们可能需要重组数据结构。
- 另外,如果你有一些Image或File字段,存储的是相对路径,那么使用 serializers.serialize 序列化时,它只会返回原始数据(也就是图片或文件的相对路径),而不会自动把 MEDIA_URL 变量定义的路径和域名信息自动带上。
- 对用户输入的数据没办法做校验。
以上说的这些工作在 DRF 中都会帮我们自动处理了,这也是 DRF 的强大之处。
再来看一下 JsonResponse 方法,它帮我们简化了一些响应处理,它做的事情也很简单,具体的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class JsonResponse(HttpResponse): def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, json_dumps_params=None, **kwargs): if safe and not isinstance(data, dict): raise TypeError( 'In order to allow non-dict objects to be serialized set the ' 'safe parameter to False.' ) if json_dumps_params is None: json_dumps_params = {} kwargs.setdefault('content_type', 'application/json') data = json.dumps(data, cls=encoder, **json_dumps_params) super().__init__(content=data, **kwargs) |
2. APIView
接下来,我们用APIView来实现。来看看借助DRF中的serializers和APIView来简化上面的操作。
1 2 3 4 5 6 7 8 |
# ./serializer.py from rest_framework.serializers import ModelSerializer from virtual.models import VirtualHost class VirtualHostSerializer(ModelSerializer): class Meta: model = VirtualHost fields = ('id', 'hostname', 'ip', 'cpu', 'memory', 'date', 'owner') |
再看借助APIView来实现的主机列表页。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# ./views.py from rest_framework.views import APIView from rest_framework.response import Response from .serializers import VirtualHostSerializer class VirtualHostView(APIView): def get(self, request, format=None): """ 通过APIView实现的主机列表页 """ data = VirtualHost.objects.all() serializer = VirtualHostSerializer(data, many=True) return Response(serializer.data) |
在APIView这个例子中,调用了DRF本身的serializer以及Response方法。其中serializer是我们自定义的,所以用起来很灵活,作用就是把数据库字段映射为Json,同时支持用户输入验证。而Response默认支持Json格式响应。
APIView对Django本身的View进行封装,从上述的代码,这样分析,两者的差别看起来不是很大,但实际中APIView做了很多东西,它定义了很多属性与方法,举几个例子。下面这三个是常用的属性:
authentication_classes:用户登录认证方式,session或者token等等。
permission_classes:权限设置,是否需要登录等。
throttle_classes:限速设置,对用户进行一定的访问次数限制等等。
到这里,可能还不能体现DRF APIView的强大之处,那么接下来的GenericAPIView就展示了它强大的功能。
3. GenericAPIView
现在我们从APIView转向更高层次的封装类,这里就要牵扯到DRF的minxins和GenericAPIView了。其中mixins实现了一些HTTP对应的动词方法,比如 get -> list、post -> create、update -> update等等。而GenericAPIView则是集测APIView,在此之上实现了更多的一些功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from rest_framework import mixins from rest_framework import generics class VirtualHostView(mixins.ListModelMixin, generics.GenericAPIView): """ 通过mixins和generics实现的主机列表页 """ queryset = VirtualHost.objects.all() serializer_class = VirtualHostSerializer def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) |
在这个例子中,继承了mixins中的ListModelMixin,就是对应把HTTP的get方法转换调用list方法,list方法会返回queryset的json数据。这里对mixins不进行过多的介绍。
GenericAPIView对APIView再次封装,实现了强大功能:
- 加入queryset属性,可以直接设置这个属性,不必再将实例化的data,再次传给seriliazer,系统会自动检测到。除此之外,可以重载get_queryset(),这样就不必设置’queryset=*’,这样就变得更加灵活,可以进行完全的自定义。
- 加入serializer_class属性与实现get_serializer_class()方法。两者的存在一个即可,通过这个,在返回时,不必去指定某个serializer。
- 设置过滤器模板:filter_backends。
- 设置分页模板:pagination_class。
- 加入 lookup_field=”pk”,以及实现了get_object方法,这个用得场景不多,但十分重要。它们两者的关系同1,要么设置属性,要么重载方法。它们的功能在于获取某一个实例时,指定传进来的后缀是什么。
举个例子,获取具体的某个课程,假设传进来的URL为:http://127.0.0.1:8000/virtual/1/,系统会默认这个1指的是virtual的id。那么,现在面临一个问题,假设我定义了一个用户收藏的Model,我想要知道我id为1的主机是否收藏了,我传进来的URL为:http://127.0.0.1:8000/userfav/1/,系统会默认获取userfav的id=1的实例,这个逻辑明显是错的,我们需要获取virtual的id=1的收藏记录,所以我们就需要用到这个属性或者重载lookup_field=”virtual_id”这个方法。
在generics除了GenericAPIView还包括了其他几个View:CreateAPIView、ListAPIView、RetrieveAPIView、ListCreateAPIView···等等,其实他们都只是继承了相应一个或多个mixins和GenericAPIView,稍微更高层次的封装类。这样,有什么好处?首先我们不需要在我们的View中同时继承mixins和GenericAPIView了,我们看一下同样一个例子的代码:
1 2 3 4 5 6 |
class VirtualHostView(generics.ListAPIView): """ 通过ListAPIView实现主机列表页 """ queryset = VirtualHost.objects.all() serializer_class = VirtualSerializer |
这样,就完成了和刚刚一模一样的功能!
4. GenericViewSet
- GenericAPIView不足之处
既然GenericAPIView以及它相关的View已经完成了许许多多的功能,那么还要ViewSet干嘛!
首先,我们思考一个问题,同样上面的例子,我们在功能上,要获取主机的列表,也要获取某个主机的具体信息。那么怎么实现,按照GenericAPIView,我们可以使用ListAPIView、RetrieveAPIView实现:
1 2 3 4 5 6 |
class VirtualHostView(generics.ListAPIView,generics.RetrieveAPIView): """ 通过ListAPIView实现主机列表页,通过RetrieveAPIView实现主机详情页 """ queryset = VirtualHost.objects.all() serializer_class = VirtualSerializer |
但这样实现有一个问题,关于serializer_class,显然,当获取主机列表时,只需要传回去所有主机的简要信息,如主机名,IP地址等等,但当获取主机的具体信息,我们还要将具体硬件配置以及这个主机谁添加的,或属于哪个机房(很明显,用户或机房属于另外一个model,有一个外键指向),这些信息会很多,在获取主机列表,将这些传回去显然是不理智的。那么,还需要再定义一个VirtualHostDetailSerializer。然后,在urls.py中定义两个匹配模式,比如在 get /virtual/ 的时候,使用VirtualHostSerializer;在 get /virtual/id/ 的时候,使用VirtualHostDetailSerializer。也就是说需要定义多个视图及URL。
那么,问题来了,我们怎么获取到是哪个action方法?这个时候,viewset就出场了!
- GenericViewSet的功能
GenericViewSet继承了GenericAPIView,依然有get_queryset,get_serialize_class相关属性与方法,GenericViewSet重写了as_view方法,可以获取到HTTP的请求方法。解决刚刚上面说的APIView的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from rest_framework import mixins from rest_framework import viewsets class VirtualHostViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): queryset = VirtualHost.objects.all() serializer_class = VirtualHostSerializer def create(self): pass def update(self): pass def partial_update(self): pass def destroy(self): pass |
从代码上可以看出,我们继承了Mixin的两个类,外加GenericViewSet类。其余的代码没变,其余的一些空方法是为了后面能够顺利进行加上的。
- http请求方法与mixins的方法进行绑定
但GenericViewSet本身依然不存在list, retrieve方法,需要我们与mixins一起混合使用,那么新问题来了?我们依然需要自己写get、post方法,然后再return list或者create等方法吗?当然不!ViewSet重写as_view的方法为我们提供了绑定的功能,我们在设置URL的时候进行绑定:
1 2 3 4 5 6 7 8 9 10 11 |
virtualhost_list = VirtualHostViewSet.as_view({ 'get': 'list', 'post': 'create' }) virtualhost_detail = VirtualHostViewSet.as_view({ 'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy' }) |
这里就把对应的HTTP方法与Mixins定义的方法进行绑定,比如说我们之前主机列表需要定义一个视图,主机详情需要定义一个视图。现在通过绑定,获取主机列表就通过list方法,而获取主机详情就通过retrieve方法,只需要一个视图即可。另外,这里进行绑定的方法,在视图中都必须存在。这也就是我上面定义一些空的方法原因。
HTTP方法绑定完之后,然后就需要在URL部分分别写获取主机列表的url,及获取主机详情的url。
1 2 3 4 5 |
urlpatterns = [ url(r'^virtual/$', virtualhost_list, name='virtualhost-list'), url(r'^virtual/(?P<pk>[0-9]+)/$', virtualhost_detail, name='virtualhost-detail'), ... ] |
这样,我们就将http请求方法与mixins方法进行了关联。那么还有更简洁的方法吗?很明显有,这个时候,route就登场了!
- route方法注册与绑定
因为我们使用ViewSet类而不是View类,实际上不用自己设计URL conf及绑定HTTP方法。连接resources到views和urls的约定可以使用Router类自动处理。我们需要做的仅仅是正确的注册View到Router中,然后让它执行其余操作。新的urls.py代码如下:
1 2 3 4 5 6 7 8 9 |
from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register(r'virtual', VirtualHostViewSet, base_name='virtualhosts') urlpatterns = [ url(r'^', include(router.urls)), ... ] |
route中使用的一定要是ViewSet,用router.register的方法注册url不仅可以很好的管理url,不会导致url过多而混乱,而且还能实现http方法与mixins中的相关方法进行连接。就拿上面注册的View来说,我们在调试模式下可以看到router帮我们生成了哪些url:
可以看到针对我们注册的url,帮我们自动生成了多个不同格式的url,其中就有我们上面定义的获取主机列表的url,及获取主机详情的url。另外,还帮我们生成了api-root相关的url(api-root是用来在我们访问根路径时展示出所有可访问url链接)。
在viewset中,还提供了两个以及与mixins绑定好的ViewSet。当然,这两个ViewSet完全可以自己实现,它只是把各类mixins与GenericViewSet继承在一起了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet): # 满足只有GET方法请求的情景 pass class ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet): # 满足所有请求都有的情景 pass |
所以在开发的时候,直接继承 viewsets.ModelViewSet 就可以了,这样就不用写list、create、update等方法了。当然,如果你的需求与DRF提供的不一致,那么你就可以重写相应的方法即可。比如,你可能需要过滤查询集,以确保只返回与当前通过身份验证的用户发出的请求相关的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class VirtualHostViewSet(ModelViewSet): """ 每个用户只可以查看owner属于自己的条目,可以创建条目 """ def list(self, request, *args, **kwargs): self.queryset = VirtualHost.objects.filter(owner=request.user.id) self.serializer_class = VirtualHostSerializer return super(VirtualHostViewSet, self).list(request, *args, **kwargs) def create(self, request, format=None): serializer = VirtualHostSerializer(data=request.data) if serializer.is_valid(): # .save()是调用VirtualHostSerializer中的create()方法 serializer.save(owner=self.request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) |
这里定义了两个方法,list方式中我们我们重写了queryset(就是对返回结果做了一次过滤),然后对于serializer_class指定了一个序列化类。并且我们使用super方法,继续载入viewset中的list方法,这里只会覆盖原有list的queryset和serializer_class。而对于create方法,我们则是完全重写了原viewset中的create方法。这里只是演示一下再ViewSet模式下如何来做工作。原则就是能用ViewSet内置的就是用内置的,内置的不满足需求就可以重写部分或全部重写。
另外,如果只是过滤查询集,最简单方法是重写.get_queryset()
方法即可。重写此方法允许你以多种不同方式自定义视图返回的查询集。
1 2 3 4 5 6 7 8 9 |
class VirtualHostViewSet(ModelViewSet): """ 每个用户只可以查看owner属于自己的条目,可以创建条目 """ serializer_class = VirtualHostSerializer def list(self, request, *args, **kwargs): return VirtualHost.objects.filter(owner=request.user.id) |
到这里,ViewSet的强大功能就介绍完了,强烈建议在做drf的时候,使用ViewSet与mixins方法结合进行开发,为我这种小白开发者提供了很强大完整的功能!
网友@__奇犽犽写了关于APIView&ViewSets的总结分析,感觉不错,转载记录一下。另外,自己改过他的文章,通过一个实例来完整阐述Django View -> APIView -> GenericAPIView -> GenericViewSet的变化。另外,关于mixins具体都做了什么,可以看看django rest framework mixins。