评论

收藏

[python] Django restframework【微创】自定义

编程语言 编程语言 发布于:2021-07-21 12:04 | 阅读数:313 | 评论:0

非常感谢你观看此文,希望此文能给你带来启发和帮助!博主自从学了python之后对Django的web框架的高自定义和强大非常赞赏,在开发了几个小的后端WEB管理应用之后,发现这种前后端不分离而且模板语言偏弱的状况给后期维护带来头疼的赶脚,痛定思痛,决定开始入手前后端分离,系统的学习了Vue.js之后,发现另一个天地,Django只专注用来做后端web API。
在研究Django RestFramework (后面简称DRF)之后有一些小小的心得分享给还在摸索中的同学,便有了此文的诞生,废话少说,步入正题:
了解DRF的同学都知道:DRF已经非常强大并且搭建简易方便,为何还要自定义呢?当然是不满足业务需求,至于本文为啥叫微创自定义,因为,我们在自定义的时候尽可能的不要去改动源码并遵从DRF的设计理念来达到目的,做到这点并不简单,很多时候恨不得把源码一改了之,痛快一点,但是尊重人家的设计规范另外也是避免出bug的一个基本原则,你不能做到比原作者更优秀那么还是谦虚点吧。
上干货,开始基础搭建:
首先,我们在pychrm开发工具软件里面创建一个Django项目BookAPI,并创建一个应用book, 一个图书管理模型为例详细讲解, 在pycharm的终端输入:
django-admin startproject BookAPI
python manage.py startapp book
不着急启动,先安装好所以必备的插件,配置好所有的参数,创建好模型再说:
安装插件:
DSC0000.jpg

配置参数:
settings.py文件中配置好以下参数:
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
        ....
  'rest_framework',
  'corsheaders',        #解决跨域问题插件
  'django_filters',      #DRF筛选过滤插件
  'book.apps.BookConfig',
]
MIDDLEWARE = [
  ....
  'django.contrib.sessions.middleware.SessionMiddleware',
  'corsheaders.middleware.CorsMiddleware',     #corsheaders跨域中间件
  'django.middleware.common.CommonMiddleware',
  ....
]
REST_FRAMEWORK = {
  'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}

CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = ()
CORS_ALLOW_METHODS = (
  'DELETE',
  'GET',
  'OPTIONS',
  'PATCH',
  'POST',
  'PUT',
  'VIEW',
)
CORS_ALLOW_HEADERS = (
  'accept',
  'accept-encoding',
  'authorization',
  'content-type',
  'dnt',
  'origin',
  'user-agent',
  'x-csrftoken',
  'x-requested-with',
)
创建模型:
from django.db import models
class Author(models.Model):
  name=models.CharField( max_length=32,blank=True)
  age=models.IntegerField(default=0)
  def __str__(self):
    return self.name

class Publish(models.Model):
  name=models.CharField(verbose_name="出版社", max_length=32,blank=True)
  city=models.CharField( max_length=32,blank=True)
  email=models.EmailField(blank=True)
  def __str__(self):
    return self.name
class Book(models.Model):
  title = models.CharField( max_length=32,verbose_name="名称",blank=True)
  pubDate=models.DateField(verbose_name="出版日期",auto_now_add=True)  #此字段和ID字段都是自动创建和赋值,前端创建和修改不用提交数据
  price=models.DecimalField(max_digits=5,decimal_places=2,verbose_name="价格",default=0)
  publish=models.ForeignKey(verbose_name="出版社",to="Publish",to_field="id",on_delete=models.CASCADE,blank=True,null=True)
  authors=models.ManyToManyField(verbose_name="所有作者",to='Author',blank=True)
  def __str__(self):
    return self.title
修改视图函数:
from rest_framework import serializers,viewsets
from book.models import *
class BookMS(serializers.ModelSerializer):
  class Meta:
    model = Book
    fields = '__all__'
class PublishMS(serializers.ModelSerializer):
  class Meta:
    model = Publish
    fields = '__all__'
class AuthorMS(serializers.ModelSerializer):
  class Meta:
    model = Author
    fields = '__all__'
修改路由url文件:
from django.urls import path,re_path
from book import views
urlpatterns = [
  path('books/', views.BookViewSet.as_view({"get": "list", "post": "create"})),
  re_path('books/(?P<pk>\d+)', views.BookViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
  })),
  path('publishs/', views.PublishViewSet.as_view({"get": "list", "post": "create"})),
  re_path('publishs/(?P<pk>\d+)', views.PublishViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
  })),
  path('authors/', views.AuthorViewSet.as_view({"get": "list", "post": "create"})),
  re_path('authors/(?P<pk>\d+)', views.AuthorViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
  })),
]
迁移完数据库后,启动项目运行,使用网址访问便可看见DRF的UI:
DSC0001.jpg

我们继续添加过滤筛选和分页功能,我们以最复杂的关联表Book来举例:
from rest_framework.pagination import PageNumberPagination
class myPagination(PageNumberPagination):
  page_size = 1
  page_query_param = 'page'
  page_size_query_param = "size"
  max_page_size = None
class BookViewSet(viewsets.ModelViewSet):
  queryset = Book.objects.all()
  serializer_class = BookMS
  pagination_class = myPagination  #分页样式
  filter_fields = ('title', 'pubDate','price','publish','authors')  #这里暂时还没用到高级过滤,只是用Django的原生筛选功能
稍微测试没有问题:
启动分页样式和过滤字段之后,刷新DRF webUI页面,可以看到多了两个东西:
DSC0002.jpg

测试功能也是正常的,通过UI组件Filters,网址链接自动加上了查询关键字:
DSC0003.jpg ,值得一提的是,它是可以联合分页一起做筛选查询的,也就是可以手动加上参数page和size一起查询:

DSC0004.jpg 依然有效。

在postman里面做测试得到序列化好的json数据:
DSC0005.jpg

可以看到,DRF默认的序列化方式将一对多和多对多关联字段自动序列成了ID和ID列表,这显然不满足需求,做过前端的都知道,你不可能就给用户渲染一个毫无意义的ID号给客户,客户至少需要知道这个ID对应的对象名称叫什么,所以,开始了本文的精髓,自定义:
在序列化类上做手脚,重写序列化类:
第一种思路:
重写to_representation函数
class BookMS(serializers.ModelSerializer):
  class Meta:
    model = Book
    fields = '__all__'
  def to_representation(self, instance):
    fields = {}
    for field in self.Meta.model._meta.fields:
      if field.name=='publish':
        fields['publish'] ={'id':getattr(instance, 'publish').id,'name':getattr(instance, 'publish').name}
      else:
        fields[field.name] = getattr(instance, field.name)
    if self.Meta.model._meta.local_many_to_many:
      for field in self.Meta.model._meta.local_many_to_many:
        if field.name=='authors':        
          fields['authors'] = getattr(instance, field.name).all().values('id','name')     
    return fields
测试查询数据,发现好用,得到我们要的键的值为对象和列表包含对象键值对,并且包含我们需要的name字段:
DSC0006.jpg

这是取数据,那么还数据呢?我们创建一条新的数据,发现问题来了:
DSC0007.jpg

报错很清楚:两个关联字段被我自定义了之后,反序列化的时候无法通过数据验证,原生的验证机制是匹配原生的反序列化过程的,要想解决此问题,要么重写验证,要么重写反序列,我选择重写Crearte函数
理由:无论是验证和反序列化都是底层封装类,改动的影响是全局的而且繁琐,不要轻易动它,我只重写创建新数据的处理函数,按照原生的验证和反序列办法将自定义的数据还原回去即可:
class BookViewSet(viewsets.ModelViewSet):
  queryset = Book.objects.all()
  serializer_class = BookMS
  pagination_class = myPagination  #分页样式
  filter_fields = ('title', 'pubDate','price','publish','authors')
   def create(self, request, *args, **kwargs):
    obj = request.data
    obj['publish'] = obj['publish']['id']
    obj['authors'] = [ author['id'] for author in  obj['authors'] ]
  #下面是源码的拷贝,我们只是在上面(data拿到request.data之前做了处理)施加了偷梁换柱,移花接木之法
    serializer = self.get_serializer(data=obj)
    serializer.is_valid(raise_exception=True)   
    self.perform_update(serializer)
    return Response(data=serializer.data)
如此巧妙的将ID还原了回去,不影响作者的任何原装功能,结果自然是成功了,当然实际业务需求中可以只需要上传ID即可,如此便可以不用做反序列化的自定义,这里只是模拟将表单数据不经任何处理提交得到的数据结构反序列化而已。
第二种思路:
对于以上业务需求的序列化结果DRF还有另一种自定义方式叫钩子函数:
class BookMS(serializers.ModelSerializer):
  class Meta:
    model = Book
    fields = '__all__'
  authors = serializers.SerializerMethodField()
  publish = serializers.SerializerMethodField()
  def get_authors(self, obj):
    return [{ 'ID':author.id,'name':author.name} for author in obj.authors.all()]
  def get_publish(self, obj):
    return {'ID': obj.publish.id, 'name': obj.publish.name}
使用get_字段名 便可以对字段的序列化方式进行自定义,这里为了追求语句的简洁使用的列表推导式,希望大家能理解其作用。
使用这种自定义的序列化方式,在不做自定义反序列化处理之前,修改和添加数据会报错误:
DSC0008.jpg

因为反序列化的时候同样会经过自定义的钩子函数处理,但此时DRF已经无法按照我自定义的逻辑得到obj,因为创建数据的时候还没有生成实例化对象,所以钩子函数在正反序列化的时候不加分别的对obj进行处理,肯定要出错,更无从取得obj.publish.id了,所以它是一个NoneType类型,可以对钩子函数动手脚吗?答案是不行,因为钩子函数的设计就是基于一个已经存在的类的实例化对象,然后自定义其字段,对于连验证提交保存一系列动作都没有执行之前的序列化过程就已经开始报错,所以,对于修改和查询数据,钩子函数好用,对于创建数据就行不通了,一定会卡在反序列化验证之前报错说空类型。
使用上面重写create函数也不好使了,因为 serializer = self.get_serializer(data=obj) 同样走的是我们自定义的那个钩子函数,前面的处理依然是无用功,报的一样的错误。
所以动用钩子函数有几个弊端:
1,会对正反系列化同时产生影响;
2,基于它自身的设计原理,无法对新创建的数据进行序列化处理,obj参数是必须;
3,会对get_serializer产生影响,因为它同样会内部调用自定义的钩子函数;
归根结底,无论如何都绕不开我们起先自定义的序列化类,进而被强制执行钩子函数。
综上所述,最简单的解决办法是,另起一个序列化类,重写获取序列化类函数,看代码就懂了:
class BookMS_Create(serializers.ModelSerializer):   #只是定义了字段,没有做任何自定义,还是验证ID的序列化类
  class Meta:
    model = Book
    fields = '__all__'
class BookViewSet(viewsets.ModelViewSet):
  queryset = Book.objects.all()
  serializer_class = BookMS
  pagination_class = myPagination  #分页样式
  filter_fields = ('title', 'pubDate','price','publish','authors')
  def get_serializer_class(self):  #重写获取序列化类函数
    # print('>>>>>>>>>>>>>>>',self.request._request.method)
    if self.request._request.method == 'POST':
      self.serializer_class = BookMS_Create
   #以下是源码的拷贝,有用没用加上最好, 别等到出了bug,不知道哪里重写了篡改人家的功能
    assert self.serializer_class is not None, (
      "'%s' should either include a `serializer_class` attribute, "
      "or override the `get_serializer_class()` method."
      % self.__class__.__name__
    )
    return self.serializer_class
  #
  def create(self, request, *args, **kwargs):
    obj = request.data
    obj['publish'] = obj['publish']['id']
    obj['authors'] = [ author['id'] for author in  obj['authors'] ]
    serializer = self.get_serializer(data=obj)
    serializer.is_valid(raise_exception=True)
    self.perform_update(serializer)
    return Response(data=serializer.data)
如此一来,当收到POST请求创建数据的时候走的是BookMS_Create这个序列化类,否则就走BookMS这个经典序列化类,如此达到对DRF微创。
测试创建数据,通过!
有人觉得第一种思路简单,一正一反,相得益彰,简单粗暴,其实第二种才是最灵活的,虽然实现上复杂了一点,但是遇到复杂多变,变态的业务需求,全部可以在get_serializer_classs这个函数里面做分支判断导向不同的自定义序列化样式类,多变灵活处理方式而且又不相互影响,多用用就知道了。
好了,有空再写其他的自定义教程,最重要的是要充分发挥DRF的功能,又要做到“微创”。


关注下面的标签,发现更多相似文章