DC's blog DC's blog
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)

DC

愿我一生欢喜,不为世俗所及.
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)
  • Django

  • 第一次学drf

  • 第二次学drf

    • drfCBV
    • 认证
    • 权限
    • 限流
    • 版本
    • 解析器
    • 元类
    • 序列化使用
    • 序列化源码
    • 验证&源码
    • 序列化+验证
      • 两大功能比较
      • 简单的Demo
      • 两个序列化器类
      • readonly&writeonly
        • 是什么?
        • 简单的示例
        • 注意的点
      • ★choice
        • 什么都不管
        • 额外字段
        • 自定义方法
      • ★外键
        • 什么都不管
        • 嵌套
        • 正确的方案
        • 错误的想法
        • 自定义方法
        • 表模型
      • 传多少问题
      • 需求来啦★
        • 需求阐述
        • 方法1
        • 方法2(推荐)
      • 更新数据
    • Serializer案例
    • Serializer总结
    • 分页
    • 视图+路由+过滤
    • 练习+跨域问题
    • 博客练习
    • 源码分析汇总
  • 温故知新

  • flask

  • 后端
  • 第二次学drf
DC
2023-11-09
目录

序列化+验证

== 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的处理:
  - 额外字段
  - 自定义方法
外键:
  - 嵌套
  - 自定义方法
  - 表模型
1
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! -- 序列化
1
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=..
1
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()),
]
1
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="人数")
1
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)
1
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)
1
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)
        """
1
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}
1
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
1
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()),
]
1
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区")))
1
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"
}
"""
1
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区"
}
"""
1
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区"
    }
}
"""
1
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()),
]
1
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
"""
1
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)
1
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
    ]
}
1
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"
        }
    ]
}
"""
1
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))
])
1
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
1
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)
1
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"
        }
    ]
}
1
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(对象) 该对象得是字典、列表之类的,若是 数据表中的对象/记录 是不支持的,会报错.
1
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)
1
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"
        }
    ]
}
1
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,不纠结了,具体场景,具体问题具体分析!!
1
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区")))
1
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区”.
1
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区"
}
"""
1
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
1
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)
1
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里的示例..

image-20230829112053593


验证&源码
Serializer案例

← 验证&源码 Serializer案例→

最近更新
01
deepseek本地部署+知识库
02-17
02
实操-微信小程序
02-14
03
教学-cursor深度探讨
02-13
更多文章>
Theme by Vdoing | Copyright © 2023-2025 DC | One Piece
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式