07 Serializer 序列化组件
一、前言
当客户端的请求来的时候,我们知道不管是get请求,还是post请求,它们携带的参数,以及数据也好,我们在后端接收到的数据都是字典类型格式的,那我们知道当我们在开发项目的时候,我们避免不了数据库中数据的存取操作。
数据库是如何存取的,我们用django框架就用django提供的orm,而orm它是对象关系映射,它是将外界的数据以字典的形式存进数据库中,又从数据库中取出数据以对象的形式给人们看的。
而我们接下来介绍的序列化组件就是围绕着数据库的记录对象来操作
当用户请求后端数据的时候,我们之前在说drf接口规范的时候,说过两个接口可以完成10个请求,当用户请求来的时候,我们看用户是向后端请求数据还是提交数据,
1. 如果用户请求数据没有携带任何参数的话,他就是请求某一类资源的所有数据
2. 如果用户在url后面拼接参数的话,它就是请求莫一类资源的莫一条具体的数据
3. 如果用户在请求后端的时候,携带了用户数据包的数据,那他就是在向后端提交数据,如果提交的url没有拼接参数,那基本上就是在向后端提交添加数据的请求,群增、群改
4. 如果用户在提交数据的时候,url又拼接参数,那就说明它可能是对某一条具体的数据进行操作
那么问题就来了,当用户请求后端的时候,我们怎么将数据返回给用户看呢,我们知道Response只能返回基本的数据类型,而我们展示给用户的数据如果是从数据库中取出来的话,对象是不可以返回的!
而用户在向后端提交数据的时候,都是以字典的形式去提交的,我们既然接收到了数据,我们肯定也要存在数据库里,可是数据库中不能直接的把字典存到数据里!!
所以就有了序列化和反序列化这一说
序列化:可以理解为将对象序列化为前台可以接收,后台可以返回的字典数据类型
反序列化:可以理解为将前台传过来的字典数据反序列化为后端可以存的对象
# 在settings中设置了国际化才可以显示中文
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
二、自定义序列化
例如我们现在已经有了一个数据库模型表User表
from django.db import models
class User(models.Model):
choice = [(0,'男'),(1,'女')]
name = models.CharField(max_length=32)
age = models.IntegerField(default=0)
height = models.DecimalField(max_digits=5,decimal_places=2,default=0)
sex = models.IntegerField(choices=choice,default=0)
icon = models.ImageField(upload_to='icon', default='default.png')
pwd = models.CharField(max_length=64,default=0)
class Meta:
# 这里db_table是将存进数据库中表改名,不再是默认的api_user
db_table='db_user'
verbose_name = '用户表'
verbose_name_plural = verbose_name
我们现在有两条url链接路径
from django.conf.urls import url
from . import views
urlpatterns = [
# 这里我们采用的是路由分发的方式
# 采用CBV的设计模式
# 我们也知道两个url可以完成10个请求
url(r'^v1/users/$',views.SelectUser.as_view()),
url(r'^v1/users/(?P<pk>\d+)/$',views.SelectUser.as_view()),
]
现在我们在前台对这两个url进行数据资源的请求,如何将后端的数据传递给前台用户
from rest_framework.views import APIView
from rest_framework.response import Response
from . import models
# 这是我们自己手动去序列化
class SelectUser(APIView):
def get(self, request, *args, **kwargs):
user_data = [] # 这里是先定义一个列表,用于返回给前台的数据
pk = kwargs.get('pk') # 这里先获取前台获取的参数,有名分组的pk=?
if pk: # 判断是否有pk值?
# 如果有值,则根据pk值去数据库中查对应的记录对象
user_obj = models.User.objects.filter(pk=pk).first()
# 判断是否有这条记录对象
if user_obj:
# 如果有,自己手动将需要返回给前台的数据,封装好,解析好打包,通过对象点取值的方式,放到我们提前定义好的user_data中
# 很麻烦
# 这个过程就是将前台的数据序列化
user_data = {
'name': user_obj.name,
'age': user_obj.age,
'sex': user_obj.sex,
'height': user_obj.height,
# 在这里,因为图片数据是不能直接返回的,所以通过拼接的方法,直接将这个图像的对象返回出去
'icon': 'http://127.0.0.1:8000%s%s' % (settings.MEDIA_URL, user_obj.icon),
}
return Response({
'status': 0,
'msg': '单查 ok',
'results': user_data
})
# 如果url没有携带参数,代表查的是数据库中所有的数据
user_list_obj = models.User.objects.all()
# 通过手动的解析,封装的方法
# 将拿到的一个对象列表for循环,
for user_obj in user_list_obj:
# 拿到列表中的每一个对象
# 把每个对象的数据,点取值后以字典的形式将字典添加到我们提前定义好的列表中
user_data.append({
'name': user_obj.name,
'age': user_obj.age,
'sex': user_obj.sex,
'height': user_obj.height,
'icon': 'http://127.0.0.1:8000%s%s' % (settings.MEDIA_URL, user_obj.icon),
})
# 不管最后有没有数据都返回数据,因为前台访问的url是成功了,只是没有数据而已
return Response({
'status': 0,
'msg': '群查 ok',
'results': user_data
})
总结:如果是少量的数据我们可以手写序列化,但是如果又大量的数据,我们手动序列化,他麻烦,而且浪费时间,开发效率极低
三、Serializer序列化组件
serializer 不是只能为数据库模型类定义,也可以为非数据库模型类定义
serializer 是独立与数据库之外的存在
我们依然以上面的user用户表为例,我们使用Serializer类来实现序列化
我们需要自定义一个类,并且必须继承Serializer这个类
from rest_framework import serializers
实例:前台向后端发送数据请求
from .Serializer import mySerializer
from rest_framework import status
class SelectUser(APIView):
def get(self, request, *args, **kwargs):
user_data = []
pk = kwargs.get('pk')
if pk:
user_obj = models.User.objects.filter(pk=pk).first()
if user_obj:
# 将user_obj 交给我们自定义的Serializer类去序列化处理
user_ser_obj = mySerializer.UserSerializer(user_obj)
# 但是序列化出来的是user_ser_obj,并不能直接返回给前台
# Serializer把真正返回给前台的数据封装在了Serializer(user_ser_obj)对象的的data属性中
user_data = user_ser_obj.data
return Response({
'status': 0,
'msg': '单查 ok',
'results': user_data
})
# 当前台的请求没有携带有名分组的参数时,代表查的是所有的数据,
user_list_obj = models.User.objects.all()
# 将user_list_obj 交给我们自定义的Serializer类去序列化处理
# 但是由于user_list_obj是一个列表[obj,obj]所以我们要给我们自己写的类多传一个参数many=True
# many在没传给UserSerializer类的时候,默认是Flase
user_ser_obj = mySerializer.UserSerializer(user_list_obj,many=True)
# 返回的仍然是user_ser_obj,并不是真正返回给前台的数据,Serializer(user_ser_obj)对象的data中
user_data = user_ser_obj.data
return Response({
'status': 0,
'msg': '群查 ok',
'results': user_data
})
# 前台发送post请求的时候,说明是向后台提交数据了
# 提交的数据报数据在request.data中,并且是querydict
# 我们这里的post请求就以向数据库中增加数据为例
def post(self, request, *args, **kwargs):
request_data = request.data
# 我们将前台提交的数据拿到request_data 字典数据拿到
# 然后传给我们自定义的反序列化的类,
# 首先是进行数据校验的,因为前台发送的数据是要创建用户,并且数据一定是由一定的存储规则的
# 先将字典数据传给反序列化类得到一个UserDescSerializer类的对象
# 注意的是,一定要明确的将request_data传给data参数,如果不传的话,它默认是将这个参数传给序列化而不是反序列化
user_ser_obj = mySerializer.UserDescSerializer(data=request_data)
# user_ser_obj 对象可以通过is_valid()方法来判别这组数据是否完全正确
if user_ser_obj.is_valid():
# 如果这组数据是正确的,一定要进行user_ser_obj.save()得到一个User类的对象
user_obj = user_ser_obj.save()
return Response({
'status':0,
'msg':'单增 ok',
'results':mySerializer.UserSerializer(user_obj).data
})
else:
return Response({
'status': 0,
'msg': user_ser_obj.errors
},status=status.HTTP_400_BAD_REQUEST)
UserSerializer:自定义的序列化/反序列化器
# 这个类就是序列化,将前台发送的数据通过后台Views的操作后从数据库中查找出来
# 然后将查出来的数据记录对象,序列化成字典,前台能够认识的数据类型,返给前台
class UserSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
age = serializers.IntegerField()
height = serializers.IntegerField()
# 这里要注意的是用户的性别这个字段,我们在存的时候,存的是数字,0和1
# 0代表男,1代表女,
# 所以在这里我们通过数据库中查出的数据对象序列化之后,发现返回给前台的依然是数字
# 所以在前台,前台可能根本就不知道0和1数字代表的是男是女,所以我们要给前台返回明确的数据
# 这个时候,我们需要重新定义一个字段,自定义的字段来获取sex对应的确切信息
# sex = serializers.IntegerField()
'''
自定义序列化字段,序列化的属性值由方法来提供,
方法的名字:固定为 get_属性名,
方法的参数:序列化对象,序列化的model对象
强烈建议自定义序列化字段名不要与model已有的属性名重名
'''
# 这样就是返回sex数字对应的信息数据了
# sex也可以用其他的名字,但是返回一定要是get_sex_display()
sex = serializers.SerializerMethodField()
def get_sex(self, obj):
print('self---', type(self)) # 这个self是我们自定义的UserSerializer对象
print('obj---', type(obj)) # 这个才是真正的User表的对象,这个对象中具有属性sex,所以
return obj.get_sex_display()
icon = serializers.FileField()
# 这个类用来将前台发送的字典数据进行反序列化,成为对象后,保存到数据库中
class UserDescSerializer(serializers.Serializer):
# 在这里所有的语法可以参考Django的forms组件
name = serializers.CharField(min_length=3,
max_length=64,
error_messages={'required': '用户名不得为空',
'min_length': '用户名不得少于3个字符',
'max_length': '用户名不得多于64个字符'})
sex = serializers.IntegerField(min_value=0,
max_value=1,
error_messages={'min_value': '性别只能存0或者1:0男-1女',
'max_value': '性别只能存0或者1:0男-1女'}
)
age = serializers.IntegerField(min_value=0,
max_value=100,
error_messages={'min_value': '年龄不得小于0',
'max_value': '年龄不得大于100'})
pwd = serializers.CharField(min_length=3,
max_length=64,
error_messages={'required': '密码不得为空',
'min_length': '密码不得少于3个字符',
'max_length': '密码不得多于64个字符'}
)
# 自定义反序列化字段:一定参与校验,且要在校验过程中,将其从入库的数据中取出,剩余与model对应的数据才会入库
re_pwd = serializers.CharField(min_length=3, max_length=64)
# 局部钩子校验方法
# 对前台发过来的name字段除了系统校验后,在进行单独的校验
def validate_name(self, value):
# value就是name值
if 'a' in value.lower(): # 判断a字母在不在name里面
# 如果在,直接抛异常
raise serializers.ValidationError('名字中不能有a字符')
# 不在就直接返回name
return value
# 全局钩子用来校验密码和确认密码是否一致
def validate(self, attrs):
pwd = attrs.get('pwd')
re_pwd = attrs.pop('re_pwd') # 将前台传过来的字典中的re_pwd的取出,并且移除
if re_pwd != pwd:
# 如果是全局校验的话,异常是以字典的形式抛出,如下
raise serializers.ValidationError({'re_pwd': '两次密码不一致'})
return attrs
# create重写,完成入库
# validated_data就是前台传过来的说一数据,并且re_pwd是已经从前台传过来的字典中去除的
def create(self, validated_data):
return models.User.objects.create(**validated_data)
四、序列化器的总结
用于序列化时:
将模型类对象传入自定义类的instance参数,它默认就是以这个参数接收模型类对象的
用于反序列化时:
将要被饭序列化的数据传入自定义类的data参数,反序列化时,一定不能把data省略
一定要是data=参数值
除了instance和data参数之外,还可以通过context参数额外序列其他的参数
ser_obj = UserDescSerializer(····,····,context={key:value,key:value})
通过context参数附加的数据,可以通过Serializer对象的context属性获取。
1. 使用序列化器的时候一定要注意,序列化器声明了以后,不会自动执行,需要我们在视图中进行调
用才可以。
2. 序列化器无法直接接收数据,需要我们在视图中创建序列化器对象时把使用的数据传递过来。
3. 序列化器的字段声明类似于我们前面使用过的表单系统。
4. 开发restful api时,序列化器会帮我们把模型数据转换成字典.
5. drf提供的视图会帮我们把字典转换成json,或者把客户端发送过来的数据转换字典
6. 直接将要序列化的数据传给序列化类
**********************************************************序列化
7. 要序列化的数据如果是单个对象,序列化的参数many为False,数据如果是多个对象(list,queryset)序列化的参数many为True
8. 序列化类的中字段必须与model中字段属性同名,如果不参与序列化的model属性,在序列化类中不做声明,也不会返回
9. 可以自定义序列化字段,自定义序列化字段用 SerializerMethodField() 作为字段类型,
序列化的属性有方法类提供,
方法的名字:get_属性名,
方法参数:序列化对象,
序列化的model对象,
注意自定义的序列化字段不要与model已有字段属性重名,返回方法的返回值
***********************************************************反序列化
1. 在视图类中,从请求对象中获取前台的数据,校验前台数据是否合法,然后将合法的数据返回给Model对象与数据进行交互,保存数据
2. 反序列化的数据如果是单个数据,反序列化的参数mangy为False,数据如果是多个字典的列表,反序列化的参数many为True
3. user_seria.errors,在报错的时候一定要先使用 iis_valid()函数
4. 反序列化类:系统的字段可以在Filed类型中设置系统校验的规则(name=serializers.CharField(min_length=3))
5. required校验规则对该字段是必校验还是可选校验字段(默认required为True,数据库字段有默认值或可以为空的字段可以将required赋值为False)
6. 自定义的反序列字段,设置系统校验规则同系统字段,但是需要在自定义校验规则中(局部、全局钩子)将自定义反序列化字段取出(返回剩余的数据与数据库交互)
7. 局部钩子的方法命名 validate_属性名(self, 属性的value),校验规则为 成功返回属性的value 失败抛出校验错误的异常
8. 全局钩子的方法命名 validate(self, 所有属性attrs),校验规则为 成功返回attrs 失败抛出校验错误的异常
9. 校验有个顺序先校验全部声明的字段,只有全部校验成功之后才会去校验局部钩子,然后在校验全局钩子;
五、序列化器的使用
序列化器的使用分两个阶段:
- 在客户端请求时,使用序列化器可以完成对数据的反序列化。
- 在服务器响应时,使用序列化器可以完成对数据的序列化。
1. 序列化:基本使用
1) 先查询出一个图书对象
from booktest.models import BookInfo
book = BookInfo.objects.get(id=2)
2) 构造序列化器对象
from booktest.serializers import BookInfoSerializer
serializer = BookInfoSerializer(book)
3)获取序列化数据:通过data属性可以获取序列化后的数据
serializer.data
# {'id': 2, 'btitle': '天龙八部', 'bpub_date': '1986-07-24', 'bread': 36, 'bcomment': 40,
'image': None}
4)如果要被序列化的是包含多条数据的查询集QuerySet,可以通过添加many=True参数补
充说明
book_qs = BookInfo.objects.all()
serializer = BookInfoSerializer(book_qs, many=True)
serializer.data
2. 反序列化:使用方法
1.数据验证
使用序列化器进行反序列化时,需要对数据进行验证后,才能获取验证成功的数据或保存成模型类对象。
在获取反序列化的数据前,必须调用is_valid()方法进行验证,验证成功返回True,否则返回False。
2.验证失败:
可以通过序列化器对象的errors属性获取错误信息,返回字典,包含了字段和字段的错误。如果是非字段错误,可以通过修改REST framework配置中的NON_FIELD_ERRORS_KEY来控制错误字典中的键名。
3.验证成功:
可以通过序列化器对象的validated_data属性获取数据。在定义序列化器时,指明每个字段的序列化类型和选项参数,本身就是一种验证行为。
3. 反序列化--局部钩子函数校验数据
局部钩子函数校验数据只对某一个字段进行校验
在反序列化的类中
def get_需要校验的字段名(self,value)
# self 是serializer类自定义类的对象
# value是需要校验的字段
if value进行一系列的判断
# 如果验证错误,则主动抛错
raise serializers.ValidationError('错误原因')
# 验证成功,直接返回数据
return value
4. 反序列化--全局钩子函数校验数据
全局钩子函数是对多个字段的值进行校验
def get_需要校验的字段名(self,attrs)
# attrs 是一个字典,存放的是需要校验的字段
# 比如
pwd = attrs.get('pwd')
re_pwd = attrs.pop('re_pwd') # 这里一般校验多个数据的时候,一般都是将确认密码这个从attrs中移除
if pwd != re_pwd:
# 多个字段校验错误的时候,是将所悟信息放在不存数据库中的数据
# 以字典的形式
raise serializers.ValidationError({'re_pwd':'错误信息'})
# 验证成功,直接返回数据
return attrs