前面已学习了基础的序列化器和各种情况下的视图处理,接下来便进一步学习序列化器,涉及一下三方面:
- 修改序列化器,控制响应数据的输出格式(数据可读性)
- 反序列化数据时的验证
- 重写序列化器自带的create和update方法
为了方便学习,需要稍微调整一下model字段。
#models.py
from django.db import models
class Company(models.Model):
company_name = models.CharField(max_length=50)
def __str__(self):
return self.company_name
class Tag(models.Model):
tag = models.CharField(max_length=30)
class User(models.Model):
STATUS_CHOICE = (
('1', '有效账号'),
('2', '无效账号')
)
user_name = models.CharField(max_length=20, unique=True)
nick_name = models.CharField(max_length=30, unique=True)
password = models.CharField(max_length=20)
company = models.ForeignKey(Company, on_delete=models.DO_NOTHING)
status = models.CharField(max_length=20, choices=STATUS_CHOICE, default=1)
tags = models.ManyToManyField(Tag)
def __str__(self) -> str:
return self.user_name
除了原来的外键,还增加了choice字段及m2m字段,以便学习各种类型的字段在序列化器中的表现。
发起一波请求,看看数据。
响应的数据,涉及到外键、m2m及choice时,只显示id和所选中的数字,没由可读性,前端拿到这个数字也毫无意义。
所以,改造序列化器的第一步,就是要提高数据的可读性,要确保给到前端的数据具有可读性,当然,也要携带相关的id才可以,不然前端发来的数据又该如何使用呢?
可读性改造
- 第一种 在序列化器中自定义字段覆盖model字段,并指定source来源
先看代码
#使用serializers.ModelSerializer自定义序列化器 简单得一批
class UserModelSerializer(serializers.ModelSerializer):
#自定义一个字段,指定来源为"get_status_display"即choice字段选项
cn_status = serializers.CharField(source='get_status_display')
class Meta:
model = User
exclude = ['password']
readonly = ('id', )
响应结果:(假设请求了id为4的user实例)
{
"id": 4,
"user_name": "POSTER",
"nick_name": "阿牛哥(PUT请求修改的)",
"cn_status": "有效帐号",
"company": {
"id": 1,
"company_name": "好运来科技有限公司"
},
...
}
显而易见,cn_status
取代了原来的status
字段。
如果只是响应请求,那么这种写法也不是不可以,毕竟cn_status
直观可读。
- 第二种 使用
serializers.SerializerMethodField
改造
SerializerMethodField继承了Field,使用SerializerMethodField是构造一个非model字段的过程,现在接着改造。
class UserModelSerializer(serializers.ModelSerializer):
cn_status = serializers.SerializerMethodField()
class Meta:
model = User
exclude = ['password']
readonly = ('id', )
def get_cn_status(self, obj):
if obj.status == '1':
return '有效帐号'
else:
return '无效账号'
注意下方的get_cn_status
的方法,这个是必须要写的,格式为get_{field_name}
,参数为当前在操作的实例。
学到这里,发现拿一个CHOICE
字段来举例,有点不太妥当。SerializerMethodField
用来显示Model
中不存在的字段,也就是自定义构造的字段,所以并不能通过反序列化来修改。其实,CHOICE
选项也就那么几个,和前端同步好对应的值不就可以了嘛。
不过,既然写了,还是请求一下,看看数据的结构吧。
{
"id": 2,
"cn_status": "有效帐号",
"user_name": "amo2",
"nick_name": "好家伙2",
"status": "1",
"company": 2
其实,这么细细一看,也不是不行,毕竟status: 1
这是可以修改的,至于cn_status: 有效帐号
只为了友好显示而已,哈哈。
- 第三种 设置模型关联深度 depth = {int}
class UserModelSerializer(serializers.ModelSerializer):
cn_status = serializers.SerializerMethodField()
class Meta:
model = User
exclude = ['password']
readonly = ('id', )
depth = 1
# 针对SerializerMethodField构造的字段,必须要写的方法,格式为 `ge_{filed_name}`
def get_cn_status(self, obj):
if obj.status == '1':
return '有效帐号'
else:
return '无效账号'
这种方法会序列化所有字段及关联模型的全部信息,发个请求看下结构。
[
{
"id": 2,
"cn_status": "有效帐号",
"user_name": "amo2",
"nick_name": "好家伙2",
"status": "1",
"company": {
"id": 1,
"company_name": "好运来科技有限公司"
},
"tags": [
{
"id": 3,
"tag": "积极的"
},
{
"id": 4,
"tag": "勇猛的"
}
]
},
{
"id": 3,
"cn_status": "有效帐号",
"user_name": "amo3333",
"nick_name": "好家伙23333",
"status": "1",
"company": {
"id": 2,
"company_name": "天天高歌电子商务有限公司"
},
"tags": [
{
"id": 1,
"tag": "激情的"
},
{
"id": 2,
"tag": "稳重的"
}
]
},
{
"id": 4,
"cn_status": "有效帐号",
"user_name": "POSTER",
"nick_name": "阿牛哥(PUT请求修改的)",
"status": "1",
"company": {
"id": 1,
"company_name": "好运来科技有限公司"
},
"tags": [
{
"id": 2,
"tag": "稳重的"
},
{
"id": 3,
"tag": "积极的"
}
]
},
{
"id": 7,
"cn_status": "有效帐号",
"user_name": "AMO",
"nick_name": "广州二五仔",
"status": "1",
"company": {
"id": 2,
"company_name": "天天高歌电子商务有限公司"
},
"tags": [
{
"id": 4,
"tag": "勇猛的"
}
]
},
{
"id": 8,
"cn_status": "有效帐号",
"user_name": "AAC",
"nick_name": "中山二五仔",
"status": "1",
"company": {
"id": 2,
"company_name": "天天高歌电子商务有限公司"
},
"tags": [
{
"id": 4,
"tag": "勇猛的"
}
]
}
]
depth
只和关联模型相关,如外键
、 多对多
等。配合着SerializersMethodField
使用,基本上能满足大部分字段的序列化/反序列化操作了。
- 第四种 嵌套序列化器
这种理念在django的admin.py
中经常使用。
#设置一个company的序列化器,然后在User的序列化器中使用
class CompanyModelSerializer(serializers.ModelSerializer):
class Meta:
model = Company
fields = "__all__"
class UserModelSerializer(serializers.ModelSerializer):
cn_status = serializers.SerializerMethodField()
#自定义字段使用company的序列化器
company = CompanyModelSerializer()
class Meta:
model = User
exclude = ['password']
readonly = ('id', )
# 针对SerializerMethodField构造的字段,必须要写的方法,格式为 `ge_{filed_name}`
def get_cn_status(self, obj):
if obj.status == '1':
return '有效帐号'
else:
return '无效账号'
这里就不发起请求了,代码改来改去有点费劲,上面这个方法最后的效果是,所嵌套的company的序列化器的全部信息能显示出来。
在实际的场景中,使用哪个序列化器,取决了实际需求,大概总结一下:
- 自定义字段 + source指定来源,这个方法会覆盖原有的字段
- SerializersMethodField + get_{field_name},构造新的非Model字段,不适用于对反序列化有修改需求的情况
- 嵌套序列化器,如果关联模型只有一个,这个方法还是非常好用的,毕竟在关联模型的序列化器中,还可以自定义
fields
来控制哪些字段不能参与序列化 - 关联模型深度设置,depth = {int},这个方法很方便,但关联模型所有的数据都会显示出来,需要做风险管理。
8月21日调整
在后面的学习中,碰到了一个问题:
class UserModelSerializer(serializers.ModelSerializer):
cn_status = serializers.SerializerMethodField()
class Meta:
model = User
exclude = ['password']
readonly = ('id', )
depth = 1
即depth = 1
时,带外键的字段会变成只读,导致反序列化保存失败。经过好几天的百度,对序列化器和视图做出以下调整:
- 重写序列化器,有只在序列化时使用的序列化器和只在反序列化时使用的序列化器
- 使用视图集来编写视图(其实也挺好用)
#序列化器
class UserModelGetSerializer(serializers.ModelSerializer):
"""
序列化时用此序列化器
"""
cn_status = serializers.SerializerMethodField()
class Meta:
model = User
exclude = ('password',)
readonly = ('id', )
depth = 1
def get_cn_status(self, obj):
if obj.status == '1':
return '有效账号'
return '无效账号'
class UserModelPostPut(serializers.ModelSerializer):
"""
反序列化时用此序列化器
"""
cn_status = serializers.SerializerMethodField()
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = '__all__'
readonly = ('id', )
def get_cn_status(self, obj):
if obj.status == '1':
return '有效账号'
return '无效账号'
#视图集
from rest_framework import viewsets
from .serializers import UserModelGetSerializer, UserModelPostPut
class UserViewSets(viewsets.ModelViewSet):
queryset = User.objects.all()
def get_serializer_class(self):
serializer_class = self.serializer_class #由于封装中内置了serializer_class,所以这里必须使用self.serializer_class,否则serializer_class就变成一个新的变量了
if self.request.method in ('PUT', 'POST',):
serializer_class = UserModelPostPut
if self.request.method in ('GET', ):
serializer_class = UserModelGetSerializer
return serializer_class
#这里需要重写update方法,用来处理前端传来的数据不包含密码修改的情况
def update(self, request, *args, **kwargs):
if not request.data.get('password'):
password = User.objects.get(id=request.data['id']).password
request.data['password'] = password
return super().update(request, *args, **kwargs)
#重写destroy 返回一个`删除`的字符串,不重写也没事,这个方法不需要调用序列化器
def destroy(self, request, *args, **kwargs):
super().destroy(request, *args, **kwargs)
return Response('删除')
#路由设置
path('v6/user/', UserViewSets.as_view({'get': 'list', 'post': 'create'})),
path('v6/user/<int:pk>', UserViewSets.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
#视图集有自带的写法,当然也可以像我这样显式编写,不过as_view需要传入字典,"请求方法": "处理动作",key和value都是内置的
这样能实现序列化时,可以使用depth = 1 带来的效果,且不输出密码选项,反序列化时就调用常规的序列化器,且自动处理密码。
好了,这个问题终于搞定了。下一章接学习数据的验证吧。
不过这里貌似使用generics.*
替代视图集或许更合适点。