62. drf之序列化组件的高阶用法

1. 序列化定制返回格式:source

1.1 基础操作:查询一条

总路由

from django.contrib import admin
from django.urls import path, includeurlpatterns = [path('admin/', admin.site.urls),path('api/v1/app01/', include('app01.urls')),  # api/v1/的含义是符合restful规范
]

app01的models.py

class Student(models.Model):name = models.CharField(max_length=32)age = models.IntegerField()school = models.CharField(max_length=32)

 序列化组件app01的serializer.py

from rest_framework import serializersclass StudentSerializer(serializers.Serializer):# 写要序列化的字段,针对Student表不一定需要将4个字段都序列化id = serializers.IntegerField()name = serializers.CharField()  age = serializers.IntegerField()school = serializers.CharField()

app01的views.py

from .models import Student
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializer import StudentSerializerclass StudentDetailView(APIView):def get(self, request, pk):# 1.根据id,取出数据student = Student.objects.filter(pk=pk).first()# 2.序列化  只有instance为序列化,只有data为反序列化的新增serializer1 = StudentSerializer(instance=student)# 3.返回给前端return Response(serializer1.data)

app01的urls.py

from django.urls import path
from .views import StudentDetailViewurlpatterns = [path('student/<int:pk>/', StudentDetailView.as_view()),
]

表中添加数据

image

查询一条数据

image

1.2 source的基础使用

[1] 方式一:在序列化组件中,前端展示字段时替换字段名称

需求:在1.1中,查询数据返回时将展示的school字段替换成scho

   用到序列化定制返回格式source

 序列化组件app01的serializer.py

class StudentSerializer(serializers.Serializer):# 写要序列化的字段,针对Student表不一定需要将4个字段都序列化id = serializers.IntegerField()name = serializers.CharField()age = serializers.IntegerField()scho = serializers.CharField(source='school')

source实现字段的替换

image

[2] 方式二:在模型表中,前端展示字段时给字段添加前缀

(1) 函数版本

app01的models.py

class Student(models.Model):name = models.CharField(max_length=32)age = models.IntegerField()school = models.CharField(max_length=32)def add_pre(self):return "high_" + self.school

序列化组件app01的serializer.py

from rest_framework import serializers
from rest_framework.exceptions import ValidationErrorclass StudentSerializer(serializers.Serializer):# 写要序列化的字段,针对Student表不一定需要将4个字段都序列化id = serializers.IntegerField()name = serializers.CharField()age = serializers.IntegerField()scho = serializers.CharField(source='add_pre')  # 将函数当成字段在使用

image

(2) 数据属性版本

使用property将以上函数伪装成数据属性

app01的models.py

class Student(models.Model):name = models.CharField(max_length=32)age = models.IntegerField()school = models.CharField(max_length=32)@propertydef add_pre(self):return "high_" + self.school

效果没有变化

image

1.3 source的跨表查询

book表的字段:name、price、出版社publish的名字(即publish的name)

假设在序列化组件中定义book表的序列化类

在book类里需要定义出版社publish字段,如果在字段中不指定source,获取到的是book.publish这个对象,而不是出版社的名字,因此要用到source

publish = serializers.CharField(source='publish.name')

2. 多表关联序列化定制返回格式

2.1 一对多定制返回格式

[1] 准备

app01的models.py

from django.db import models# Create your models here.class Publish(models.Model):name = models.CharField(max_length=32)addr = models.CharField(max_length=32)city = models.CharField(max_length=32)class Book(models.Model):name = models.CharField(max_length=32)price = models.IntegerField()publish = models.ForeignKey(to='Publish', on_delete=models.SET_NULL, null=True)authors = models.ManyToManyField(to='Author')class Author(models.Model):name = models.CharField(max_length=32)age = models.IntegerField()addr = models.CharField(max_length=32)

迁移至数据库,添加数据

image

image

image

image

[2] 一对多代码实现

出版社和图书是一对多  写个查询图书的接口---返回出版社信息

序列化组件app01的serializer.py

from rest_framework import serializersclass BookSerializer(serializers.Serializer):name = serializers.CharField(max_length=32)price = serializers.IntegerField()publish = serializers.CharField(source='publish.addr')  # source可以取publish表的所有字段

app01的views.py

from .models import Book
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializer import BookSerializerclass BookDetailView(APIView):def get(self, request, pk):# 1.根据id,取出数据book = Book.objects.filter(pk=pk).first()# 2.序列化  只有instance为序列化,只有data为反序列化的新增serializer1 = BookSerializer(instance=book)# 3.返回给前端return Response(serializer1.data)

app01的urls.py

from .views import BookDetailView
from django.urls import pathurlpatterns = [path('book/<int:pk>/', BookDetailView.as_view()),
]

序列化组件查询publish.addr

image

序列化组件查询publish.city

image

 

[3]需求:通过书籍表的序列化组件获取出版社的所有字段name、addr、city/作者表,放到一个字典或列表中

(1) 方式一:模型表中写函数

在模型表中写一个函数(可以使用:property),函数有返回值(字典、字符串、列表)

序列化组件中,使用DictField、ListField、CharField接收返回值(字典、列表、字符串),但是CharField为纯字符串,不能被调用

字典版本:在模型表中写函数,在书籍表的序列化组件中使用DictField接收

对models.py的book表加以修改

class Book(models.Model):name = models.CharField(max_length=32)price = models.IntegerField()publish = models.ForeignKey(to='Publish', on_delete=models.SET_NULL, null=True)authors = models.ManyToManyField(to='Author')@property  # 将函数属性变为数据属性def publish_detail(self):return {'name': self.publish.name, 'addr': self.publish.addr, 'city': self.publish.city}

序列化组件app01的serializer.py

class BookSerializer(serializers.Serializer):name = serializers.CharField(max_length=32)price = serializers.IntegerField()publish_detail = serializers.DictField()

返回的publish数据为字典

image

列表版本:在书籍表的序列化组件中使用ListField接收

对models.py的book表加以修改

class Book(models.Model):name = models.CharField(max_length=32)price = models.IntegerField()publish = models.ForeignKey(to='Publish', on_delete=models.SET_NULL, null=True)authors = models.ManyToManyField(to='Author')@property  # 将函数属性变为数据属性def authors_detail(self):info = []for i in self.authors.all():info.append({"name": i.name, "age": i.age, "addr": i.addr})return info

序列化组件app01的serializer.py

class BookSerializer(serializers.Serializer):name = serializers.CharField(max_length=32)price = serializers.IntegerField()authors_detail = serializers.ListField()

返回的数据格式为列表

image

 

(2) 方式二:序列化组件中写函数

写一个字段,对应的字段类是:SerializerMethodField
必须对应一个 get_字段名 的函数,函数必须接受一个obj,函数的返回值什么,这个字段对应的值就是什么

obj就是当次序列化得到的book对象,可以看成模型表中的self

models.py的book表保持不变

class Book(models.Model):name = models.CharField(max_length=32)price = models.IntegerField()publish = models.ForeignKey(to='Publish', on_delete=models.SET_NULL, null=True)authors = models.ManyToManyField(to='Author')

序列化组件app01的serializer.py

class BookSerializer(serializers.Serializer):name = serializers.CharField(max_length=32)price = serializers.IntegerField()publish_detail = serializers.SerializerMethodField()# 函数的返回值是什么,publish_detail的值就是什么def get_publish_detail(self, obj):return {'name': obj.publish.name, 'addr': obj.publish.addr, 'city': obj.publish.city}

get后面的名称必须和之前的字段名保持一致,否则报错

image

image

(3) 方式三:子序列化

定义出版社的序列化组件,将出版社序列化组件当作变量的值存放在书籍的序列化组件中

模型表无需任何额外操作

app01的serializer.py中

class PublishSerializer(serializers.Serializer):name = serializers.CharField()addr = serializers.CharField()city = serializers.CharField()class BookSerializer(serializers.Serializer):name = serializers.CharField(max_length=32)price = serializers.IntegerField()publish = PublishSerializer()  # 字段名必须为外键名publish

image

2.2 多对多定制返回格式

[1] 多对多分析

图书和作者是多对多  写个查询图书的接口---返回作者信息

总路由,app01的urls.py、views.py与一对多保持不变

[2] 方式一:模型表中写函数

由于接口定义在图书查询中,因此函数写在图书的模型表中

app01的models.py

class Book(models.Model):name = models.CharField(max_length=32)price = models.IntegerField()publish = models.ForeignKey(to='Publish', on_delete=models.SET_NULL, null=True)authors = models.ManyToManyField(to='Author')# 写返回作者列表的函数
    @propertydef author_l(self):l = []for i in self.authors.all():  # 获取所有的作者l.append({'name': i.name, 'age': i.age, 'addr': i.addr})return l

序列化组件app01的serializer.py

class BookSerializer(serializers.Serializer):name = serializers.CharField(max_length=32)price = serializers.IntegerField()author_l = serializers.ListField()

id为1的图书对应2个作者,返回的数据格式为列表

image

[3] 方式二:序列化组件中写函数

写一个字段,对应的字段类是:SerializerMethodField
必须对应一个 get_字段名 的函数,函数必须接受一个obj,函数的返回值什么,这个字段对应的值就是什么

obj就是当次序列化得到的book对象,可以看成模型表中的self

由于接口定义在图书查询中,因此函数写在图书的序列化组件中

models.py的book表无需修改

class Book(models.Model):name = models.CharField(max_length=32)price = models.IntegerField()publish = models.ForeignKey(to='Publish', on_delete=models.SET_NULL, null=True)authors = models.ManyToManyField(to='Author')

序列化组件app01的serializer.py

class BookSerializer(serializers.Serializer):name = serializers.CharField(max_length=32)price = serializers.IntegerField()author_l = serializers.SerializerMethodField()# 函数的返回值是什么,author_l字段的值就是什么def get_author_l(self, obj):l = []for i in obj.authors.all():  # 获取所有的作者l.append({'name': i.name, 'age': i.age, 'addr': i.addr})return l

image

[4] 方式三:子序列化

定义作者的序列化组件,将作者序列化组件当作变量的值存放在图书的序列化组件中

模型表无需任何额外操作

app01的serializer.py中,多对多一定要加上many=True

class AuthorSerializer(serializers.ModelSerializer):  # 子序列化组件name = serializers.CharField()age = serializers.IntegerField()addr = serializers.CharField()class BookSerializer(serializers.Serializer):name = serializers.CharField()price = serializers.IntegerField()authors = AuthorSerializer(many=True)  # 字段名必须为外键名authors

image

3. ListField介绍

3.1 概念

反序列化(接收前端数据,data=xxx + is_valid()输入校验),必须写 child
仅序列化输出(只传instance,只读不校验输入),可以不写 child,但不推荐
ListField代表整个字段是一个列表,child规定列表里面每一项是什么类型。

3.2 语法

serializers.ListField(child= 列表内部字段对象)

3.3 代码

[1] 场景1:只用来做序列化输出(只读,只向前端返回,不接收前端传入)

# 可以运行,但不规范
authors_detail = serializers.ListField()

只输出没问题;一旦用于 post 校验直接报错。

规范写法:

authors_detail = serializers.ListField(child=serializers.DictField(), read_only=True)

read_only=True:这个字段只用于序列化输出,反序列化直接忽略该字段,不会用来接收前端数据。

[2]场景2:只用来做前端输入的反序列化校验,不返回给前端(只写)

city = serializers.ListField(child=serializers.IntegerField(), write_only=True)

post 的时候接收列表,serializer.data序列化返回给前端时不会带出这个字段。

3.4 post与get的ListField对比

[1] POST 接口(反序列化,必须 child)

# ✅正确
authors = serializers.ListField(child=serializers.IntegerField())# ❌错误,post调用is_valid直接报错
authors = serializers.ListField()

[2] GET接口(仅序列化输出)

# 能运行,但是不规范
authors_detail = serializers.ListField()# ✅规范,read_only标记只读
authors_detail = serializers.ListField(child=serializers.DictField(), read_only=True)

3.5 常用参数

参数作用
child 列表内部元素字段,反序列化必填
allow_empty=True 是否允许空列表[],默认 True;设 False 不允许空数组
read_only=True 仅输出,不接收前端传入
write_only=True 仅接收输入,不输出

3.6 总结

ListField()括号内不加参数,只有纯读场景可以运行;只要做校验 is_valid,一定报错

 

4. 多表关联反序列化

4.1 多表关联反序列化之新增 

[1] 需求:新增图书

book表的字段为

image

前端传给后端的validated_data为{'name':'信息论', price:666, 'publish_id':1, 'authors':[1, 2]}

前端传过来的publish_id要与数据库中的字段保持一致

 

app01的models.py

from django.db import models# Create your models here.class Publish(models.Model):name = models.CharField(max_length=32)addr = models.CharField(max_length=32)city = models.CharField(max_length=32)class Book(models.Model):name = models.CharField(max_length=32)price = models.IntegerField()publish = models.ForeignKey(to='Publish', on_delete=models.SET_NULL, null=True)authors = models.ManyToManyField(to='Author')class Author(models.Model):name = models.CharField(max_length=32)age = models.IntegerField()addr = models.CharField(max_length=32)class Student(models.Model):name = models.CharField(max_length=32)age = models.IntegerField()school = models.CharField(max_length=32)

app01的urls.py

from .views import BookView
from django.urls import pathurlpatterns = [path('book/', BookView.as_view()),
]

[2] 错误示范

(1) 报错代码

视图函数中序列化返回给前端,序列化组件中serializers.ListField( )不加参数

序列化组件app01的serializer.py

from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.Serializer):# 前端传入的图书数据格式为{'name':'信息论', price:666, 'publish_id ':1, 'authors':[1, 2]}name = serializers.CharField()price = serializers.IntegerField()publish_id = serializers.IntegerField()  # 数据库的字段名为publish_idauthors = serializers.ListField()# 重写create函数# validated_data是字典,即前端传入经过校验的数据(以上字典中的内容),保存到指定的表中# **validated_data对数据进行解包def create(self, validated_data):# 为了和数据库中book表对应,需要剔除publish和authors# 1.前端数据第一部分保存到book表authors = validated_data.pop('authors')  # authors已从validated_data剔除book = Book.objects.create(**validated_data)  # 一对多反序列化,书与出版社# 2.前端数据第二部分保存到关联表book_authors,注意create与add的用法book.authors.add(*authors)  # 多对多反序列化,书与作者return book

app01的views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from .serializer import BookSerializerclass BookView(APIView):def post(self, request):# 1.取出前端传入的请求体中的数据(json、urlencoded、form-data)# 2.前端传入的图书数据格式为{'name':'信息论', price:666, 'publish_id':1, 'authors':[1, 2]}#   外键字为id,非实际值# 3.括号参数只有instance为序列化,只有data为反序列化的新增# 4.序列化组件使用前端传入的数据request.data实例化得到对象serializer1 = BookSerializer(data=request.data)# 5.反序列化校验、保存到数据库、返回给前端# 6.不知道要保存到哪个表中,要在序列化组件中重写create方法#   serializer1.save如果是新增,会触发serializer.create方法的运行if serializer1.is_valid():  # 反序列化(输入校验)
            serializer1.save()return Response(serializer1.data)  # 序列化(输出)  读取data,返回给前端else:return Response(serializer1.errors)

image

image

(2) 报错原因

校验阶段:serializer.is_valid()拿 request.data 做反序列化(输入校验),ListField 不知道列表里面是什么类型,校验失败;ListField必须声明内部元素类型,ListField()括号内不加参数序列化校验直接报错,is_valid()返回 False。
就算绕过校验,serializer1.data把 book 实例做序列化输出:同样ListField没有参数,DRF 不知道列表authors里的数据该怎么序列化格式化输出,执行return Response(serializer1.data)时直接抛出异常。
简而言之:ListField()不能空参,必须加上参数,否则校验、序列化输出 (serializer.data) 两步都会出错。
加上child=serializers.IntegerField(),告诉 DRF:列表内部是整数,输入校验、输出序列化两者全部正常工作。

(3) 结论

只做序列化输出(读),不做反序列化(数据接收校验),ListField( )不加参数可以运行;
serializers.ListField( )如果既要接收前端提交数据 (反序列化),又要序列化输出(读),ListField( )不加参数会报错。

[3] 修正

(1) 返回字符串

将serializer1.data 改为普通字符串返回,代码也能运行

class BookView(APIView):def post(self, request):serializer1 = BookSerializer(data=request.data)if serializer1.is_valid():  # 反序列化(输入校验)
            serializer1.save()return Response("ok") else:return Response(serializer1.errors)

调用 is_valid() 不会抛出 Python 异常导致代码崩溃,而是直接返回 False,错误放在 serializer1.errors(低于DRF3.10的版本输入校验时ListField 强制要求有参数,DRF3.10以后的版本不强制要求有参数)

1.serializer1.is_valid()运行,遇到 authors = serializers.ListField(), 没有参数
2.ListField 反序列化校验失败,给 errors 增加错误信息,函数返回 False
3.进入 else 分支,返回错误信息,永远不会运行到 return Response ("ok")
关键点:
is_valid() 一定会运行,一定会检测到 ListField 缺少参数,但是不会崩溃,只是返回 False;
return Response("ok") 是 if 内部代码,校验失败根本不会运行。

(2) 给serializers.ListField加上参数

class BookSerializer(serializers.Serializer):name = serializers.CharField()price = serializers.IntegerField()publish_id = serializers.IntegerField()# 只改这一行,加上参数authors = serializers.ListField(child=serializers.IntegerField())def create(self, validated_data):authors = validated_data.pop('authors')book = Book.objects.create(**validated_data)book.authors.add(*authors)return book

加上child=serializers.IntegerField(),告诉 DRF:列表内部是整数,输入校验、输出序列化理论上可以运行,输出序列化还存在数据格式的问题

serializer1.data序列化输出时,直接拿模型authors多对多字段(是ManyRelatedManager对象),序列化器顶层没有定义authors输出规则,DRF 原生序列化无法直接迭代ManyRelatedManager管理器,触发TypeError: 'ManyRelatedManager' object is not iterable。
运行链路:
1.serializer.save()正常执行入库、多对多绑定,数据库无问题。
2.运行return Response(serializer1.data)读取序列化输出。
3.序列化器BookSerializer里:authors字段是只用于入参反序列化的ListField(用来接收前端传入 ID 数组做新增);
序列化输出(正向序列化,模型转 JSON)时,DRF 尝试用这个ListField去读取模型实例的book.authors;
book.authors是ManyRelatedManager,不是普通列表,ListField内部尝试遍历直接报错。
总结:同一个authors字段,入参用来接收 ID 列表,出参要读取多对多管理器,类型不匹配直接迭代报错。

(3) 直接移除序列化输出的 authors 字段,返回不要作者列表

class BookSerializer(serializers.Serializer):name = serializers.CharField()price = serializers.IntegerField()publish_id = serializers.IntegerField()# 只改这一行,加上参数authors = serializers.ListField(child=serializers.IntegerField(), write_only=True)def create(self, validated_data):authors = validated_data.pop('authors')book = Book.objects.create(**validated_data)book.authors.add(*authors)return book

只返回书籍基础字段,不会处理多对多字段,无报错。
write_only=True:该字段仅反序列化(接收前端数据)生效,正向序列化.data直接跳过该字段,不会去读取模型的authors关联管理器,彻底规避报错。

image

image

4.2 序列化组件传值补充

如果前端传的字段比序列化组件和数据库中的字段要多,多了个addr,{"name":"三国演义","price":19,"publish_id":1,"authors":[1,2],"addr":"上海"}
到了序列化组件create函数中,validated_data中得到的数据也不包含该字段addr:{"name":"三国演义","price":19,"publish_id":1,"authors":[1,2]}

4.3 多表关联反序列化之修改

[1] 前言

image

image

image

image

以book模型表为切入点修改数据

  修改book的基础数据

  修改book的出版社

  修改book的作者

[2] 代码

app01的urls.py

from .views import BookDetailView
from django.urls import pathurlpatterns = [path('book/<int:pk>/', BookDetailView.as_view()),
]

app01的views.py

from .models import Book
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializer import BookSerializerclass BookDetailView(APIView):def put(self, request, pk):# 前端传过来用于修改的数据的格式{'name':'信息论', price:666, 'publish_id':1, 'authors':[1, 2]}# 1.根据id,查询需要修改的对象book = Book.objects.filter(pk=pk).first()# 2.获取前端传入的数据,使用data数据对instance对象进行修改serializer1 = BookSerializer(data=request.data, instance=book)# 3.校验数据# 4.保存到数据库中if serializer1.is_valid():# 需要在序列化组件中重写update方法
            serializer1.save()return Response(serializer1.data)else:return Response(serializer1.errors)

序列化组件app01的serializer.py

class BookSerializer(serializers.Serializer):# 序列化组件在进行反序列化校验时,以下字段需要和前端传入的字段一一对应# 前端传入的图书数据格式为{'name':'信息论', price:666, 'publish_id ':1, 'authors':[1, 2]}name = serializers.CharField()price = serializers.IntegerField()publish_id = serializers.IntegerField()  # 数据库的字段名为publish_idauthors = serializers.ListField(child=serializers.IntegerField(), write_only=True) # 只写# 重写update函数def update(self, instance, validated_data):""":param instance: 待修改的对象:param validated_data:前端传入,校验过后的数据:return:"""# 1.为了和数据库中book表对应,需要剔除authorsauthors = validated_data.pop('authors')  # authors已从validated_data剔除# 2.修改book表for k in validated_data:# 3.利用反射方法  等同于instance.k=validated_data.get(k)
            setattr(instance, k, validated_data.get(k))# 4.修改中间表book_authorsinstance.authors.set(authors)  # set将原来数据先有移除,再新增,等同于clear+add(*authors)
        instance.save()return instance  # 返回修改的对象

对book表id为3的记录进行修改

image

image

image