序列化+验证
== 1 ==
在“简单的Demo”小节. 这是一个仅关乎‘验证’功能的Demo,且关联数据库的字段都是些普通的字段.
== 2 ==
接着,我们想着如何编写代码, 同时实现 验证+序列化 两个功能.
首先要明白:
其一 默认read_only=False, write_only=False ; ModelSerializer自动生成的id字段read_only=True.
其二 提到验证功能, 想到得让用户输入; 提到序列化功能,想到返回给前端展示
其三 理清源码流程
- 无论是序列化功能还是验证功能,都会调用ModelSerializer中的get_fields方法,获取所有字段对象
- ser._writable_fields 可获得执行验证功能时,序列化类所用的字段对象
- 筛选依据 read_only=True的不要. 因为read_only=True的字段只能序列化,不能验证.
- 验证通过后,这些字段对象 进行save存储时 “k”字段对象的source属性值:“v”前端传入的该字段值
(**validated_data进行拆分后就是k=v形式)
- ser._readable_fields 可获得执行序列化功能时,序列化类所用的字段对象
- 筛选依据 write_only=True的不要. 因为write_only=True的字段只能验证,不能序列化.
- 序列化后 在前端进行展示 “k”字段对象的变量值:“v”instance.source
想要同时实现 验证+序列化 两个功能.有两个方式,没有优劣之分,就需求而言,代码怎么清晰怎么来.
方式一 > 写两个序列化类,一个用于校验,一个用于序列化
方式二 > 写一个序列化类,用read_only和write_only来区分哪些字段只读,哪些字段只写
在最后,做了一点优化.
(不用将序列化器类实例化两次,实例化一次即可.
因为,save成功后,内部会自动将DpModelSerializer的instance赋值成ser.save()的返回值,即db中添加的那条记录.)
== 3 ==
开始进阶,前面的示例中关联数据库的字段都是些普通的字段. 像choice字段、1:n,n:n的外键字段 如何处理?
choice的处理:
- 额外字段
- 自定义方法
外键:
- 嵌套
- 自定义方法
- 表模型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
同时进行序列化和校验
# 两大功能比较
序列化 + 数据校验.
-功能1:序列化-
路由 --> 视图 --> 从数据库中获取对象或QuerySet --> ‘序列化器类’将其转换为可序列化成json格式的数据类型
-功能2:数据校验-
路由 --> 视图 --> request.data --> ‘序列化器类’校验 --> 操作(添加、更新db)
请求经过路由到达视图, 视图中可通过request.data拿到该请求的请求体中的所有数据.
需要通过序列化器类对这些数据进行校验.校验成功后进行一些操作.(写入数据库/更新数据库的数据)
Ps:虽然Django内置的Form和ModelForm组件就可以对数据进行校验,但drf的开发者也没用它,而是自己写了一套.
因为,有些场景会将校验和序列化一起使用.
-功能1和功能2的结合使用- 以用户注册为例.
{"username":"","password":""} 校验成功后,存入数据库. -- 数据校验
(db中新增的这个obj中有用户名密码,还有自增的ID、注册时间等),取一部分序列化后返回给用户.可作为Token! -- 序列化
2
3
4
5
6
7
8
9
10
11
12
13
★ 结合源码的流程分析出来的一丢丢注意点
功能1和功能2,都先要经历 在序列化源码中的前3个步骤 创建字段对象、基于元类创建序列化器类、创建序列化器类的对象!!
经历着三个步骤后,接下来.
当执行ser.data以及执行ser.is_valid代码时,接下来的源码执行流程中都会拿到序列化器里所有可用的字段对象!
“但要知道其本质都是通过Serializer类里的fields方法拿到的!!“
(获取所有字段对象过程基本一致,那么意味着那些drf规则两个功能都是一视同仁的!)
-- 注意区分序列化器类继承的是ModelSerializer还是Serializer
注:★不管继承的是啥,在获取所有字段对象阶段,你序列化器类里定义了啥字段类型的类变量都可以,都不会报错!!
why不报错, ModelSerializer因为源码里那个continue! ‘回想一下,此时已经拿到了所有字段名字,去循环 里面有个continue的’
Serializer因为源码里直接返回的_declared_fields里的内容.
注:继承的是ModelSerializer,会利用Model来自动生成字段对象. ‘在continue后面’
(_declared_fields中有的字段名跟数据库的相同的话,不会生成.直接用_declared_fields里的那个字段对象)
(“特别注意,前面是使用model生成对象,没有用model对应数据库里的值,何时用db里的值,循环过程 序列化、更新等)
拿到序列化器里所有可用的字段对象后
功能1对其序列化 序列化的数据"字段值从数据库中取", - 序列化器类实例化时候传递的是 instance=..
功能2对其验证 验证的数据 "字段值是请求中传来的", - 序列化器类实例化时候传递的是 data=..
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 简单的Demo
先来一个简单的仅关乎验证的Demo,在这个Demo的基础上逐渐实现 验证+序列化.
注: ser.save() 新增数据成功后, 会返回当前添加成功的数据对象!
url.py 根路由
from django.urls import path
from api import views
urlpatterns = [
path('api/<str:version>/dp/', views.DpView.as_view()),
]
2
3
4
5
6
api/models.py
from django.db import models
class Depart(models.Model):
title = models.CharField(verbose_name="部门", max_length=32)
order = models.IntegerField(verbose_name="顺序")
count = models.IntegerField(verbose_name="人数")
2
3
4
5
6
7
api/views.py
class DpModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Depart
fields = "__all__"
class DpView(APIView):
"""http://127.0.0.1:8001/api/v1/dp/ POST请求 发送Json数据 {"title":"事业部","order":6,"count":11}"""
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
print(ser.validated_data) # OrderedDict([('title', '事业部'), ('order', 6), ('count', 11)])
# 新增数据成功后,会返回当前成功添加的数据对象!
# 该数据对象是所使用的序列化器类DpModelSerializer的Meta里指定的model,即Depart里的一条记录.
save_instance = ser.save()
print(save_instance) # Depart object (6)
return Response("success.")
else:
return Response(ser.errors)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 两个序列化器类
我们通常会将ser.save() 新增数据成功后, 返回的当前添加成功的数据对象进行序列化, 然后返回给前端!!
有一个方法, 就是编写两个序列化器, 一个用于做数据校验, 一个用于做序列化.
校验时使用校验的序列化类 DpModelSerializer
; 给前端返回数据时, 使用另一个序列化类Dp2ModelSerializer
.
class DpModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Depart
fields = "__all__" # 指定需要校验的字段
class Dp2ModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Depart
fields = ["id", "title", "count"] # 指定需要序列化的数据字段
class DpView(APIView):
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
print(ser.validated_data) # OrderedDict([('title', '事业部'), ('order', 7), ('count', 11)])
save_instance = ser.save()
print(save_instance) # Depart object (7)
ser2 = Dp2ModelSerializer(instance=save_instance)
return Response(ser2.data) # {"id":7,"title":"事业部","count":11}
else:
return Response(ser.errors)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# read_only&write_only
我们通常会将ser.save() 新增数据成功后, 返回的当前添加成功的数据对象进行序列化, 然后返回给前端!!
虽说可以通过两个序列化器类来实现, 但未免太麻烦了..
我们可以通过给字段对象添加属性read_only=True或write_only=True来实现 验证 + 序列化!!
# 是什么?
read_only=True - 读 仅序列化不验证
表明 只有 在序列化时使用. 意味着前端可以不传该字段, 该字段不用进行校验.
write_only=True - 写 仅验证不序列化
表明 只有 在校验时才会使用. 意味着前端需要传该字段, 并对其进行校验, 但不会对该字段序列化, 即展示时,该字段不会显示.
(提到验证功能, 想到得让用户输入; 提到序列化功能, 想到返回给前端展示)
应用场景:
新用户注册时, 需要填写 邮箱、手机号、密码、重复密码等. 后台注册成功后, 返还给前端的用户信息不应该有密码、重复密码..
# 简单的示例
class DpModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Depart
fields = ["id", "title", "order", "count"]
extra_kwargs = {
"id": {"read_only": True},
"count": {"write_only": True},
}
def get_fields(self):
res = super(DpModelSerializer, self).get_fields()
print("获取所有字段对象 >>:", res)
return res
class DpView(APIView):
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
# ★ read_only=True的不要. 因为read_only=True的字段只能序列化,不能验证.
print('验证功能,序列化类所用的字段对象 >>:', list(ser._writable_fields))
if ser.is_valid():
print("ser.validated_data >>:",ser.validated_data)
save_instance = ser.save()
ser2 = DpModelSerializer(instance=save_instance)
# ★ write_only=True的不要. 因为write_only=True的字段只能验证,不能序列化.
print('序列化功能,序列化类所用的字段对象 >>:', list(ser2._readable_fields))
print('ser.data >>:', ser2.data)
return Response(ser2.data) # {"id":7,"title":"事业部","order":10}
else:
return Response(ser.errors)
"""★ 这部分代码可以优化:
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
ser.save() # -- save成功后,内部会自动给DpModelSerializer的instance成功赋值成ser.save()的返回值,即对象.
return Response(ser.data)
else:
return Response(ser.errors)
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
http://127.0.0.1:8001/api/v1/dp/
POST请求 发送Json数据 {"title":"事业部","order":7,"count":10}
Django控制台打印结果如下:
获取所有字段对象 >>:
OrderedDict([
('id', IntegerField(label='ID', read_only=True)),
('title', CharField(label='部门', max_length=32)),
('order', IntegerField(label='顺序')),
('count', IntegerField(label='人数', write_only=True))])
验证功能,序列化类所用的字段对象 >>:
[CharField(label='部门', max_length=32),
IntegerField(label='顺序'),
IntegerField(label='人数', write_only=True)]
ser.validated_data >>:
# - ※我觉得: 元祖的第一项应该是字段对象中source_attrs属性的值;元祖里的第二项应该是经过校验后前端传递的字段值.
OrderedDict([('title', '事业部'), ('order', 7), ('count', 10)])
获取所有字段对象 >>:
OrderedDict([
('id', IntegerField(label='ID', read_only=True)),
('title', CharField(label='部门', max_length=32)),
('order', IntegerField(label='顺序')),
('count', IntegerField(label='人数', write_only=True))])
序列化功能,序列化类所用的字段对象 >>:
[IntegerField(label='ID', read_only=True),
CharField(label='部门', max_length=32),
IntegerField(label='顺序')]
ser.data >>:
# - ※我觉得: 字典的key值应该是字段对象的变量名;字典的value值应该是 instace.字段souce_attrs 的值.
{"id":7,"title":"事业部","order":10}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 注意的点
首先要知道:
看了源码, Field类的__init__
初始化方法里
默认read_only=False, write_only=False , 也就是说, 默认字段是 可读/序列化 and 可写/前端得传,验证成功后可存储的.还有一点, 若设置
fields = "__all__"
. ModelSerializer自动帮忙生成的id字段对象, 其read_only=True.
思考下为啥?一开始, 让用户输入id吗,不, 因为没必要,id在数据库里是自增的, 不用用户传.
So, id字段read_only=True.仅序列化不验证.
根据上述打印结果进行分析:
1> 上面程序中有两次对序列化器类的实例化,会执行两次get_fields方法.两次获取的所有字段对象的结果是一样的.
2> 验证功能 ,序列化类所用的字段对象 只要read_only=False的
换个思路, 好理解些: read_only=True的不要. 因为read_only=True的字段只能序列化,不能验证.
3> 序列化功能 ,序列化类所用的字段对象 只要只要write_only=False的
换个思路, 好理解些: write_only=True的不要. 因为write_only=True的字段只能验证,不能序列化.
关于,序列化器类实例ser_obj的_writable_fields
方法和_readable_fields
方法!!
看源码,它们分别会在ser_obj.is_valid
和ser_obj.data
后调用!! 关键源码如下: (可佐证上面的第2第3条结论.)
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
def fields(self):
fields = BindingDict(self)
# -- 继承ModelSerializer的话,使用的是ModelSerializer中的get_fields方法,获取所有字段对象
for key, value in self.get_fields().items():
fields[key] = value
return fields
# -- 获得执行验证功能时,序列化类所用的字段对象
@property
def _writable_fields(self):
for field in self.fields.values():
if not field.read_only:
yield field
# -- 获得执行序列化功能时,序列化类所用的字段对象
@property
def _readable_fields(self):
for field in self.fields.values():
if not field.write_only:
yield field
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ★choice
开始进阶,前面的示例中关联数据库的字段都是些普通的字段. 那么像choice字段, 如何处理?
url.py 根路由
from django.urls import path
from api import views
urlpatterns = [
path('api/<str:version>/dp/', views.DpView.as_view()),
]
2
3
4
5
6
api/models.py Depart数据库做了一点改变, 添加了area字段, 表明该部分属于A区还是B区.
from django.db import models
class Depart(models.Model):
title = models.CharField(verbose_name="部门", max_length=32)
order = models.IntegerField(verbose_name="顺序")
count = models.IntegerField(verbose_name="人数")
area = models.SmallIntegerField(verbose_name="区域", choices=(("1", "A区"), ("2", "B区")))
2
3
4
5
6
7
8
# 什么都不管
按照DpModelSerializer序列化器类里的配置分析.
"id" | "title" | "order" | "count" | "area" | |
---|---|---|---|---|---|
前端展示 | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
前端必传,验证,存数据库 | ✖️ | ☑️ | ☑️ | ☑️ | ☑️ |
需求: 根据程序执行结果, 可以看到 area字段展示的是数字 2.. 不是我们想要, 我们想让它展示2对应在内存中的文本数据"B区".
class DpModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Depart
fields = ["id", "title", "order", "count", "area"]
extra_kwargs = {
"id": {"read_only": True},
}
class DpView(APIView):
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
""" POST请求 http://127.0.0.1:8001/api/v1/dp/ 传递Json数据:{"title":"事业部","order":7,"count":10,"area":2}
{
"id": 20,
"title": "事业部",
"order": 7,
"count": 10,
"area": "2"
}
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 额外字段
按照DpModelSerializer序列化器类里的配置分析.
"id" | "title" | "order" | "count" | "area" | "area_text" | |
---|---|---|---|---|---|---|
前端展示 | ☑️ | ☑️ | ☑️ | ☑️ | ✖️ | ☑️ |
前端必传,验证,存数据库 | ✖️ | ☑️ | ☑️ | ☑️ | ☑️ | ✖️ |
area字段只写, 前端得传, 但不会在前端展示.. 额外字段area_text, 只读, 前端不用传, 能在前端展示.
class DpModelSerializer(serializers.ModelSerializer):
"""
Q: 思考,此处能否写成,area = serializers.CharField(source="get_area_display") ???
A:
- 首先要明确,这样写 序列化的功能是没有问题的, 因为该字段序列化的本质是 instance.get_area_display()
- 但这样写 验证没问题,存储到数据库时会报错:
经过实践,会在ser.save()处报错,Depart() got an unexpected keyword argument 'get_area_display'
ser.save()的本质是 models.Depart.objects.create(**ser.validated_data)
打印ser.validated_data,可得知其值为
OrderedDict([('title', '事业部'), ('order', 7), ('count', 10), ('get_area_display', '2')])
Depart表中没有get_area_display字段,所以报错.
内心OS:
根据结果,是将字段对象的source值作为了元祖的第一项的值. 前一篇博文的最后有这方面的推导心路历程.
"""
area_text = serializers.CharField(source="get_area_display", read_only=True)
class Meta:
model = models.Depart
fields = ["id", "title", "order", "count", "area", "area_text"]
extra_kwargs = {
"id": {"read_only": True},
"area": {"write_only": True},
}
class DpView(APIView):
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
""" POST请求 http://127.0.0.1:8001/api/v1/dp/ 传递Json数据:{"title":"事业部","order":7,"count":10,"area":2}
{
"id": 21,
"title": "事业部",
"order": 7,
"count": 10,
"area_text": "B区"
}
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 自定义方法
按照DpModelSerializer序列化器类里的配置分析.
"id" | "title" | "order" | "count" | "area" | "area_info" | |
---|---|---|---|---|---|---|
前端展示 | ☑️ | ☑️ | ☑️ | ☑️ | ✖️ | ☑️ |
前端必传,验证,存数据库 | ✖️ | ☑️ | ☑️ | ☑️ | ☑️ | ✖️ |
特别注意 , SerializerMethodField 默认 read_only=True.
class DpModelSerializer(serializers.ModelSerializer):
"""
Q: 思考,此处能否写成下面这样???
area = serializers.SerializerMethodField()
def get_area(self, obj):
return obj.get_area_display()
A: 不行.
SerializerMethodField字段对象里设置了read_only=True,序列化是没有问题的.
但在save时,会报错,说没有area字段的值.为啥?
因为,验证会 筛除掉 read_only=True的字段!! 所以 ser.validated_data 通过验证的字段是没有area的!!
Ps,那我耍点小聪明,area = serializers.SerializerMethodField(read_only=False),这样写呢?
不行,没有效果的,看看源码就知道了,哪怕你指定了read_only=False,源码中也会将此值覆盖.“kwargs['read_only'] = True”
"""
# - ★查看SerializerMethodField源码,可以看到该类型的字段对象默认设置了read_only=True
area_info = serializers.SerializerMethodField()
def get_area_info(self, obj):
# - 用自定义方法的好处就是可自行构造数据结构返回.
return {
"area_input": obj.area,
"area_text": obj.get_area_display()
}
class Meta:
model = models.Depart
fields = ["id", "title", "order", "count", "area", "area_info"]
extra_kwargs = {
"id": {"read_only": True},
"area": {"write_only": True},
}
class DpView(APIView):
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
"""POST请求 http://127.0.0.1:8001/api/v1/dp/ 传递json数据 {"title":"事业部","order":7,"count":10,"area":2}
{
"id": 35,
"title": "事业部",
"order": 7,
"count": 10,
"area_info": {
"area_input": "2",
"area_text": "B区"
}
}
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# ★外键
外键字段 按照我们的心意进行展示! 不是显示数据库中存储的数据.是显示该数据对象关联的数据..
有三个方向的解决思路.
1> 其一是嵌套;
2> 其二是自定义方法;
3> 其三是在表模型中操作.
url.py 根路由
from django.urls import path
from api import views
urlpatterns = [
path('api/<str:version>/book/', views.BookView.as_view()),
]
2
3
4
5
6
api/models.py
from django.db import models
class Publish(models.Model):
title = models.CharField(max_length=32)
class Author(models.Model):
name = models.CharField(max_length=32)
class Book(models.Model):
title = models.CharField(max_length=32)
publish = models.ForeignKey(to='Publish', on_delete=models.CASCADE)
authors = models.ManyToManyField(to='Author')
# -- 使用脚本添加了以下数据
"""
- app01_publish
id title
1,四川出版社
2,重庆出版社
- app01_author
id name
1,wpq
2,zk
- app01_book
id title publish
1,三十六计,1
2,西游记,2
- app01_book_author
id book_id author_id
1,1,1
2,1,2
3,2,1
4,2,2
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 什么都不管
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from app01 import models
class AuthorModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Author
fields = "__all__"
class BookModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Book
fields = ["id", "title", "publish", "authors"]
extra_kwargs = {
"id": {"read_only": True},
}
class BookView(APIView):
def post(self, request, *args, **kwargs):
ser = BookModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
http://127.0.0.1:8001/api/v1/book/
POST请求 发送json数据 {"title":"红楼梦","publish":1,"authors":[1,2]}
需求: 根据程序执行结果, 可以看到1对多的外键字段publish展示的是数字 1.. 多对多的外键字段authors展示的是一个列表[1,2]
这不是我们想要, 我们想让它展示这些数据背后对应的对象具体的信息!!
{
"id": 9,
"title": "红楼梦",
"publish": 1,
"authors": [
1,
2
]
}
2
3
4
5
6
7
8
9
# 嵌套
# 正确的方案
"publish", "authors" 用于校验写入数据库; "publish_info", "authors_info" 用于读取.
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from app01 import models
class PublishModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Publish
fields = "__all__"
class AuthorModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Author
fields = "__all__"
class BookModelSerializer(serializers.ModelSerializer):
publish_info = PublishModelSerializer(source="publish", read_only=True)
authors_info = AuthorModelSerializer(many=True, source="authors", read_only=True) # 注意many参数.因为是n:n
class Meta:
model = models.Book
fields = ["id", "title", "publish", "authors", "publish_info", "authors_info"]
extra_kwargs = {
"id": {"read_only": True},
"publish": {"write_only": True},
"authors": {"write_only": True},
}
class BookView(APIView):
def post(self, request, *args, **kwargs):
ser = BookModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
"""
{
"id": 8,
"title": "红楼梦",
"publish_info": {
"id": 1,
"title": "四川出版社"
},
"authors_info": [
{
"id": 1,
"name": "wpq"
},
{
"id": 2,
"name": "zk"
}
]
}
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 错误的想法
注意, 我是如何思考为什么此方案行不通的心理流程..
若我们在BookModelSerializer序列化器内通过 重写get_fields方法, 可得到 BookModelSerializer中的所有字段对象.
它长下面这个样子 (主要是我想观察, 外键字段对象 以及 嵌套得到的 字段对象, 长什么样子)
OrderedDict([
('id', IntegerField(label='ID', read_only=True)),
('title', CharField(max_length=32)),
('publish', PrimaryKeyRelatedField(queryset=Publish.objects.all(), write_only=True)),
('authors', ManyRelatedField(allow_empty=False,
child_relation=PrimaryKeyRelatedField(
allow_empty=False,
queryset=Author.objects.all(),
write_only=True),
write_only=True)),
('publish_info', PublishModelSerializer(read_only=True, source='publish'):
id = IntegerField(label='ID', read_only=True)
title = CharField(max_length=32)),
('authors_info', AuthorModelSerializer(many=True, read_only=True, source='authors'):
id = IntegerField(label='ID', read_only=True)
name = CharField(max_length=32))
])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
那引发了我一个思考, 若将嵌套的序列化器 BookModelSerializer 像下面这样写, 是否可以?
于是乎, 我进行了实践, 答案是, 不行!!
class BookModelSerializer(serializers.ModelSerializer):
"""
思考: 此处能否写成这样?
publish = PublishModelSerializer()
authors = AuthorModelSerializer(many=True)
A: 在此 验证+序列化 的场景中 行不通.
首先,在只进行序列化的那篇博文中,示例证明了这种写法序列化是能通过的. 那么就证明是验证出了问题.
看报错,走的是ser.is_valid()的值为False时,return Response(ser.errors)返回错误信息的分支.
所以,我们知道它不会是save报的错,是字段验证过程出的错.
再继续分析,
我们知道 验证过程是 先进行字段1的"自身的校验+局部钩子校验",然后是字段2的,以此类推,最后是全局校验.
我做了实验,写了"title", "publish", "authors的局部钩子和最后的全局钩子.
根据打印结果,发现,title字段的局部钩子执行了的,"publish"的局部钩子并没有执行!
★ 证明,这样的写法,嵌套后,publish字段和authors字段自身的校验是没办法通过的.
这两个字段都会报错.
{
"publish":{"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]},
"authors":{"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]}
}
心得:一开始,我想着去找源码,看non_field_errors是怎么添加进字段错误的,很麻烦很费时,最后花了时间也没解决.
所以,明白大体流程,像上述一样一步步分析,知道是因为哪个环节报错就行啦,不用知道源码底层是如何实现的.不要本末倒置.
"""
publish = PublishModelSerializer()
authors = AuthorModelSerializer(many=True)
class Meta:
model = models.Book
fields = ["id", "title", "publish", "authors"]
extra_kwargs = {
"id": {"read_only": True},
}
def validate_title(self, value):
print("111", value)
return value
def validate_publish(self, value):
print("222", value)
return value
def validate(self, attrs):
print('333', attrs)
return attrs
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 自定义方法
class BookModelSerializer(serializers.ModelSerializer):
publish_title = serializers.CharField(source="publish.title", read_only=True)
authors_obj = serializers.CharField(source="authors.all", read_only=True)
publish_info = serializers.SerializerMethodField(source="publish", read_only=True)
authors_info = serializers.SerializerMethodField(source="authors", read_only=True)
def get_publish_info(self, obj): # # obj --> Book object (20)
return {
"id": obj.publish.pk,
"title": obj.publish.title
}
def get_authors_info(self, obj): # obj --> Book object (20)
authors_info = []
# ★ obj.authors.all() 正向跨表拿到所有的Author对象
# <QuerySet [<Author: Author object (1)>, <Author: Author object (2)>]>
for author_obj in obj.authors.all():
authors_info.append({
"id": author_obj.pk,
"name": author_obj.name,
})
return authors_info
class Meta:
model = models.Book
fields = ["id", "title", "publish", "authors",
"publish_title", "authors_obj",
"publish_info", "authors_info"]
extra_kwargs = {
"id": {"read_only": True},
"publish": {"write_only": True},
"authors": {"write_only": True},
}
class BookView(APIView):
def post(self, request, *args, **kwargs):
ser = BookModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
http://127.0.0.1:8001/api/v1/book/
POST请求 发送json数据 {"title":"红楼梦","publish":1,"authors":[1,2]}
{
"id": 21,
"title": "红楼梦",
"publish_title": "四川出版社",
"authors_obj": "<QuerySet [<Author: Author object (1)>, <Author: Author object (2)>]>",
"publish_info": {
"id": 1,
"title": "四川出版社"
},
"authors_info": [
{
"id": 1,
"name": "wpq"
},
{
"id": 2,
"name": "zk"
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 表模型
在表对应的模型类中写 , 方法返回什么, 这个字段就是什么!!
其实本质跟 前面“自定义方法“ 的逻辑差不多.
Book表模型添加两个方法 publish_detail、authors_detail
from django.db import models
class Publish(models.Model):
title = models.CharField(max_length=32)
class Author(models.Model):
name = models.CharField(max_length=32)
class Book(models.Model):
title = models.CharField(max_length=32)
publish = models.ForeignKey(to='Publish', on_delete=models.CASCADE)
authors = models.ManyToManyField(to='Author')
# @property # -- 是否包装成数据属性都行.可加可不加.
def publish_detail(self):
return {
"id": self.publish.pk,
"title": self.publish.title
}
"""
若跨表过去的publish表字段太多,懒得写,可以曲线救国!!
(注意,若publish中还有有外键,__dict__中该字段值是对象,对象是不能json序列化!!还需手动处理.)
p_dict = self.publish.__dict__
p_dict.pop("_state")
return p_dict
"""
# @property
def authors_detail(self):
return [
{
"id": obj.pk,
"name": obj.name
} for obj in self.authors.all()
]
# ps:model模型表里的__str__,是print打印数据表中的对象用的,让其打印时输出字符串.“因为打印对象,默认输出的是一堆内存地址啥的.”
# 序列化时内部会调用 json.dumps(对象) 该对象得是字典、列表之类的,若是 数据表中的对象/记录 是不支持的,会报错.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
视图逻辑如下.
class BookModelSerializer(serializers.ModelSerializer):
# - 这两个字段跟Book模型表中的两个方法是一一对应的.
# - 注意: 它两的source默认就是publish_detail、authors_detail
publish_detail = serializers.DictField(read_only=True)
authors_detail = serializers.ListField(read_only=True)
class Meta:
model = models.Book
fields = ["id", "title", "publish", "authors", "publish_detail", "authors_detail"]
extra_kwargs = {
"id": {"read_only": True},
"publish": {"write_only": True},
"authors": {"write_only": True},
}
class BookView(APIView):
def post(self, request, *args, **kwargs):
ser = BookModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
http://127.0.0.1:8001/api/v1/book/
POST请求 发送json数据 {"title":"红楼梦","publish":1,"authors":[1,2]}
{
"id": 27,
"title": "红楼梦",
"publish_detail": {
"id": 1,
"title": "四川出版社"
},
"authors_detail": [
{
"id": 1,
"name": "wpq"
},
{
"id": 2,
"name": "zk"
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 传多少问题
经过源码的洗礼, 也许某个场景下, 有这么个需求:
Depart表中 有字段id、title、order、count、area.. 先ser.is_valid、成功后接着ser.save、ser.data 1> 用户传递的数据没有area
2> 用户传递的数据多了字段 xxx
注意, 数据库Depart表在设计时, order字段可为空! 意味着创建 Depart对象时,可以不需要该字段的值.. 即前端不用传.
# - 少了area,多了xxx
if ser.is_valid():
a = ser.validated_data.pop("xxx")
ser.save(area=1)
ser.instance.xxx = a
return Response(ser.data)
- 思考注意几点
2> read_only=True,那么用户就铁定不传, 写了read_only这个字段又传该字段的值,那算什么个事?这种场景得不到应用,不要多想.
3> 新增的数据时
ser.validated_data与数据库字段相比,多了,就提前pop;少了,就给save()传参
4> 新增后序列化
ser.instance 拿到的是 db中新增的那个对象
该db对象中可没有xxx,因为save之前pop掉了.
源码 attribute = field.get_attribute(instance)会报错
会说,ser.instance这个'Depart' object has no attribute 'xxx'. 取不到值!
- 要么 ser.instance.xxx = a 加上;
- 要么 设置xxx字段是 write_only=True
ok,不纠结了,具体场景,具体问题具体分析!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 需求来啦★
有些需求是需要知道源码的流程才能实现的!! - 简单来说,传入的名和输出的名一样.
choice属性的字段、外键字段, 在序列化时, 想按照我们想的展示的内容, 而不是直接展示数据库中存储的.
可以通过额外字段指定source或者自定义方法来实现. 但这仅适用于纯序列化的时候..
当我们加入验证时, 验证 + 序列化. 若还这么操作. 那么:
用额外字段指定source的方案, 在save时,会报错表中没有该source名字的字段;
用自定义方法的方案, 因为自定义字段的read_only默认等于True, save时会报错缺少该字段..
最简单的就是用下述的方法2, 你前端按照ORM的规则正常传值存储到数据库, 但取每个序列化的结果时, 直接截获做一点处理..
此小节示例使用的数据库.
from django.db import models
class Depart(models.Model):
title = models.CharField(verbose_name="部门", max_length=32)
order = models.IntegerField(verbose_name="顺序")
count = models.IntegerField(verbose_name="人数")
area = models.SmallIntegerField(verbose_name="区域", choices=(("1", "A区"), ("2", "B区")))
2
3
4
5
6
7
8
# 需求阐述
前景回顾/遗留问题
在上面choice那一小节, 额外字段以及自定义方法 这两个方案中.. 我们思考过
area = serializers.CharField(source="get_area_display")
序列化能通过, 但save时报错 .
area = serializers.SerializerMethodField()
+ get_area
方法, 序列化能通过, 但save时报错 ..
为啥报错, (详情请看对应部分阐述) ..
**需求 **
但这让人很不得劲, 我就是想 编写一个序列化类, 实现 部门信息的添加. 返回前端的数据, 就用area.其值是对应内存中的数据.
提供: {"title":"事业部","order":7,"count":10,"area":2}
返回: {"id": 21, "title":"事业部","count":10,"area":"B区"}.
area字段既要经历验证也要经历序列化!
部门信息添加 - 先校验再序列化返回.
提供 - 根据提供的这个进行校验
返回 - 根据返回的这个,作为序列化的结果,用户前端展示
返回相比于提供的,order字段消失,area的值从2变为了“B区”.
2
3
4
5
怎么做呢?
# 方法1
需要知道, SerializerMethodField 与它绑定的钩子方法 实现的细节..
该源码分析过程 在8_序列化源码.md这篇博文的最后的“一些思考”的小节里狠狠的分析过了! 哈哈哈.还需要复习下, 拿到所有可以序列化的字段对象后, 进行循环的过程中对每个字段对象做了什么?!!
关键就在于 get_attribute和to_representation方法! 简单来说, get_attribute能想到的是 instance.xxx.xxx.
第一点:
分析源码, 发现得到所有可序列化的字段对象后, 会依次执行字段对象的get_attribute方法和to_representation方法..
attribute = field.get_attribute(instance)
ret[field.field_name] = field.to_representation(attribute)
get_attribute方法的返回值会当作to_representation的attribute参数的实参.
第二点:
分析源码, 自定义方法与字段的绑定主要通过 字段对象在执行bind方法时绑定的!!
另外, 调用自定义方法, 用到了反射!! 自定义方法的参数是一个instance对象, 哪里可以拿到呢? get_attribute方法里.
第三点:
该字段对象应该继承serializers.IntegerField.. 因为数据库area字段是models.SmallIntegerField. 即db中存的是int类型的数字.
这样的话, area字段的值是int类型的数据2, 才能通过 get_area_display() 去数据库中比对拿到在内存中的值, 字符串的"2"不行..
所以, 我们自己写一个字段对象, 至少需要重写bind、get_attribute、to_representation方法
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from api import models
class NbIntegerField(serializers.IntegerField):
def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
super().__init__(**kwargs)
def bind(self, field_name, parent):
if self.method_name is None:
# -- 这里设置了 绑定方法的默认命名
self.method_name = 'dc_get_{field_name}'.format(field_name=field_name)
super().bind(field_name, parent)
def get_attribute(self, instance):
method = getattr(self.parent, self.method_name)
return method(instance)
def to_representation(self, value):
return str(value)
class DpModelSerializer(serializers.ModelSerializer):
area = NbIntegerField()
def dc_get_area(self, obj):
return obj.get_area_display()
class Meta:
model = models.Depart
fields = ["id", "title", "order", "count", "area"]
extra_kwargs = {
"id": {"read_only": True},
"order": {"write_only": True},
}
class DpView(APIView):
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
"""POST请求 http://127.0.0.1:8001/api/v1/dp/ 发送JSON数据 {"title":"事业部","order":7,"count":10,"area":2}
{
"id": 46,
"title": "事业部",
"count": 10,
"area": "B区"
}
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
内心OS, 哪怕我知道流程, 让我自己改写, 自己也写不出来.. 我尝试过, 很天真的以为, 在源码的基础上做点改动就行了.
比如 想着通过一个类继承SerializerMethodField, 重写init方法, 在super的后面写上self.read_only=Flase.进行了覆盖..
改是改变了. 但运行的时候, 使用该类的字段的校验会报错.. 报错我还看不懂.. 只能定位到是字段自身校验出的错.. 折腾半天,只能说此方案行不通..
老师的改写, 让我意识到要注重看函数的输入和输出!
# 方法2(推荐)
想法: 得到所有可序列化的字段对象后,会循环. 那我能否能在循环体里, 先判断xx开头的钩子函数是否存在?
在项目根目录下创建文件夹ext, 并创建文件 hook.py , 写入一下内容.
from collections import OrderedDict
from rest_framework.fields import SkipField
from rest_framework.relations import PKOnlyObject
class NbHookSerializer(object):
def to_representation(self, instance):
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
"""也就在执行源码本来的代码之前,判断了下nb开头的钩子是否存在. 一共多写了4行代码."""
if hasattr(self, 'nb_%s' % field.field_name):
value = getattr(self, 'nb_%s' % field.field_name)(instance)
ret[field.field_name] = value
else:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
视图层逻辑
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from api import models
from ext.hook import NbHookSerializer
# - 把NbHookSerializer类里面的to_representation写在DpModelSerializer类里效果一样.
# 但提取出来,可以多处使用,更方便!NbHookSerializer专门提供这个功能.
class DpModelSerializer(NbHookSerializer, serializers.ModelSerializer):
class Meta:
model = models.Depart
fields = ["id", "title", "order", "count", "area"]
extra_kwargs = {
"id": {"read_only": True},
"order": {"write_only": True},
}
# - nb_字段名
def nb_area(self, obj):
return obj.get_area_display()
class DpView(APIView):
def post(self, request, *args, **kwargs):
ser = DpModelSerializer(data=request.data)
if ser.is_valid():
ser.save()
return Response(ser.data)
else:
return Response(ser.errors)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 更新数据
图来自第一次学drf时, ModeSerializer里的示例..