python django django-rest-framework

django rest framework serializers小结

技术分享

systemime
2021-06-03
17 min

django-rest-framework serializers序列化小结

# 引言

serializers 是什么?这是官网介绍

Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types.

简单一句话就是,将复杂的数据结构变成 json 或者 xml 这个格式的

serializers 大致有以下作用:

  • querysetmodel 实例等进行序列化,转化成 json 格式,返回给用戶 (api 接口)。
  • postpatch/put 的上來的上来的数据进行校验
  • postpatch/put 数据进行处理
  • listretrieve 等方法快速获取数据对象,并进行操作

先张图简单介紹一下主要內容

img.png

# 一、serializers.field

我們知道在 django 中,form 也有许多 field,那 serializers 其实也是 drf 中发挥着这样的功能。我們先简单了解常用的几个 field

# 1. 常用的 field

CharFieldBooleanFieldIntegerFieldDateTimeField 这几个用得比较多,外键的 field 后面说

from rest_framework import serializers

mobile = serializers.CharField(max_length=11, min_length=11)
age = serializers.IntegerField(min_value=1, max_value=100)

pay_time = serializers.DateTimeField(read_only=True,format='%Y-%m-%d %H:%M')
is_hot = serializers.BooleanField()

不同的是,我们在django中,form 更强调对提交的表单进行一种认证,而 serializer 的 field 同时作用于数据的传入与返回

# 2. Core arguments參數

serializers的每一种field,如CharFieldDateTimeField等,都继承自Field父类,其__init__ 方法如下

import re

class empty:
    """
    This class is used to represent no data being provided for a given input
    or output value.
    此类用于表示没有为给定输入或输出值提供数据

    It is required because `None` may be a valid input or output value.
    它是必需的,因为 None 可能是有效的输入或输出值
    """
    pass

REGEX_TYPE = type(re.compile(''))

# 不能同时设置`read_only` 和 `write_only`
NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
# 不能同时设置 `read_only` 和 `required`
NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
# 不能同时设置`required` 和 `default`
NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
# Field(read_only=True) 应该是 ReadOnlyField
USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField'
# ValidationError 由 `{class_name}` 引发,但错误键 `{key}` 在 `error_messages` 字典中不存在
MISSING_ERROR_MESSAGE = (
    'ValidationError raised by `{class_name}`, but error key `{key}` does '
    'not exist in the `error_messages` dictionary.'
)

class Field:
    _creation_counter = 0

    default_error_messages = {
        'required': _('This field is required.'),
        'null': _('This field may not be null.')
    }
    default_validators = []
    default_empty_html = empty
    initial = None

    def __init__(
        self, read_only=False, write_only=False,
        required=None, default=empty, initial=empty, source=None,
        label=None, help_text=None, style=None,
        error_messages=None, validators=None, allow_null=False
    ):
        self._creation_counter = Field._creation_counter
        Field._creation_counter += 1

        # If `required` is unset, then use `True` unless a default is provided.
        # 如果未设置 `required`,则除非提供默认值,否则使用 `True`
        if required is None:
            required = default is empty and not read_only

        # Some combinations of keyword arguments do not make sense.
        # 验证某些没有意义的关键字组合
        assert not (read_only and write_only), NOT_READ_ONLY_WRITE_ONLY
        assert not (read_only and required), NOT_READ_ONLY_REQUIRED
        assert not (required and default is not empty), NOT_REQUIRED_DEFAULT
        assert not (read_only and self.__class__ == Field), USE_READONLYFIELD

        self.read_only = read_only
        self.write_only = write_only
        self.required = required
        self.default = default
        self.source = source
        self.initial = self.initial if (initial is empty) else initial
        self.label = label
        self.help_text = help_text
        self.style = {} if style is None else style
        self.allow_null = allow_null

        if self.default_empty_html is not empty:
            if default is not empty:
                self.default_empty_html = default

        if validators is not None:
            self.validators = list(validators)

        # These are set up by `.bind()` when the field is added to a serializer.
        # 当字段被添加到序列化器时,这些由 `.bind()`方法 设置
        self.field_name = None
        self.parent = None

        # Collect default error message from self and parent classes
        # 从自身和父类收集默认错误消息
        messages = {}
        for cls in reversed(self.__class__.__mro__):
            messages.update(getattr(cls, 'default_error_messages', {}))
        messages.update(error_messages or {})
        self.error_messages = messages

我们可以看到支持的参数有read_onlywrite_onlyrequireddefaultinitialsource, labelhelp_textstyleerror_messagesvalidatorsallow_null

经常使用的有如下几种:

  • read_only:默认False,True 表示不允许用户上传,只能用于api输出。如果某个字段设置了read_only=True,那么就不需要进行数据验证,只会在返回时,将这个字段序列化后返回
      举个例子:用户购物时post订单,肯定会产生一个订单号,而这个订单号应该由后台生成,而不应该由用戶post过来,如果不设置 read_only=True,验证的时候就会报错。

    order_no = serializers.CharField(readonly=True)
    
  • write_only: 与 read_only 对应,仅输入时验证,在response时不序列化该字段

  • required: 该字段是否必填,从上面源码可知,在没有设置默认值并不是 read_only=True 时,默认为True

  • allow_null/allow_blank:是否允许为None/为空

  • error_messages:自定义错误提示,如

    order_no = serializers.CharField(
        required=True, min_length=10,
        allow_null=False,
        error_messages={
            "required": "订单号是必填项",
            "null": "请正确填写订单号",
            "min_length": "订单号不能小于10个字符"
        }
    )
    

下面是作用于html表单控制

  • label: 字段显示设置,用作HTML表单字段或其他描述性元素中的字段名称,如 label="验证码"
  • help_text: 可用作在HTML表单字段或其他描述性元素中对该字段进行描述的文本字符串,在指定字段增加一些提示文字
  • style: 说明字段的类型,键值对字典,可用于控制渲染器应如何渲染字段,如下:
    password = serializers.CharField(style={'input_type': 'password'})
    color_channel = serializers.ChoiceField(
        choices=['red', 'green', 'blue'],
        style={'base_template': 'radio.html'}
    )
    

基于Field父类实现的 CharFieldDateTimeFieldBooleanField 等 Field 还拥有自定义等一些参数,可以具体查看

在Field中,还有一个至关重要的参数 validators, 可以实现一些更加灵活的序列化字段校验,可以看下面的介绍

# 3. Validation 自定义验证逻辑

# 单独的validate方法

我們在上面提到field,它能起到一定的验证作用,但很明显,它存在很大的局限性

简单例子,我們要判断手机号,如果使用CharField(max_length=11, min_length=11),只能确保用户输入11位字符,不能证明这是一个手机号

实际上可以这样做

class OrderSerializer(serializers.Serializer):
    mobile_phone = serializers.CharField(max_length=11, min_length=11)

    # 注意,方法名必须是 validate_{序列化字段名} 的形式
    def validate_mobile_phone(self, mobile_phone):
        rule = (
            r"^([京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新使领]{1}[A-Z]{1})"
            r"([A-HJ-NP-Z0-9]{5}|[DF][A-HJ-NP-Z0-9][0-9]{4}|[0-9]{5}[DF])$"
        )
        pattern = re.compile(rule)
        if pattern.match(mobile_phone.upper()) is None:
            raise serializers.ValidationError("车牌号不合法")
        return mobile_phone

在这个方法里可以任意实现你想要的验证逻辑

# 使用 validators 参数

validators是一个可迭代对象,直接写在Field参数中,和上面单独的validate方法作用差不多

def multiple_of_ten(value):
    if value % 10 != 0:
        raise serializers.ValidationError('Not a multiple of ten')

class GameRecord(serializers.Serializer):
    score = serializers.IntegerField(validators=[multiple_of_ten])

drf提供的validators还有很好的功能:UniqueValidator,UniqueTogetherValidator等,如

  • UniqueValidator: 指定某一个对象是唯一的,如,用戶名只能存在唯一

    username = serializers.CharField(
            max_length=11, 
            min_length=11,
            validators=[UniqueValidator(queryset=UserProfile.objects.all())
    )    
    
  • UniqueTogetherValidator: 联合唯一,如用戶收藏某个课程,这时候就不能单独作用于某个字段,我们需要在Meta中设置

    class XXSerializer(serializers.Serializer):
        ......
    
        class Meta:
            validators = [
                UniqueTogetherValidator(
                    queryset=User.objects.all(),
                    fields=('user', 'course'),
                    message='已收藏'
                )
            ]
    

# 使用validate方法

上面的验证方式只能验证一个字段,如果多个 输入 字段联合验证,我们就需要重载 validate 方法

class XXSerializer(serializers.Serializer):
    start = serializers.DateTimeField()
    finish = serializers.DateTimeField()

    def validate(self, attrs):
    
        if attrs['start'] > attrs['finish']:
            raise serializers.ValidationError("finish must occur after start")
        return attrs

依靠重载 validate方法,我们可以再在这里对一些 read_only 字段进行操作

如:自动生成的订单号

class XXSerializer(serializers.Serializer):
    order_no = serializers.CharField(readonly=True)

    def validate(self, attrs):
        attrs['order_no'] = generate_order_no()
        return attrs

modelserializer 中,可以剔除掉 write_only 的字段,这个字段仅用于验证,但不存在于指定的model中,即不能save(),可以在这pop掉

# 4. HiddenField

HiddenField 的值不依靠输入,而需要设置默认的值,不需要用户自己post数据过来,也不会显示返回給用戶,最常用的就是User!!

我们在登陆情况下,假如一个用户去收藏了某一门课,那么系统应该自动识别该用户,然后用戶只需要将课程的id 串过来,这个需求,我们配合CurrentUserDefault()实现

# 这样就可以直接获取到当前用户
"""
- serializers.CurrentUserDefault()
  获取当前请求中User对象

- from django.contrib.auth import get_user_model
  ManagerUser = get_user_model()
  
  获取当前活动的User模型对象
"""

user = serializers.HiddenField(
    default=serializers.CurrentUserDefault())

# 二、save instance

这是官方一个小标题,可看出,这是为了 postpatch 所设置的,如果仅get请求,前面的field可能已经满足需求 参考view 以及 mixins的博客, post请求对应create方法,而patch请求对应update方法,这里的create与update方法,是指mixins中特定类中的方法

源码如下,源代码分析也可以参考mixins

# 只截取一部分
class CreateModelMixin(object):
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

class UpdateModelMixin(object):
    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()

可以看出,无论是是create与update都写了一行:serializer.save(),那么,这一行到底做了什么事情?

# serializer.py
def save(self, **kwargs):
# 略去一些稍微无关的内容
    ···
    if self.instance is not None:
        self.instance = self.update(self.instance, validated_data)
            ···
    else:
        self.instance = self.create(validated_data)
            ···
    return self.instance

显然 serializer.save的操作,它去调用了 serializercreateupdate 方法,不是 mixins 中的。

我们看一下流程图(以post为例):

序列化保存流程图

在实际业务开发者,这两个方法就是让你重载的,(也建议尽量不要去重载 to_representation 方法)

如果你的 viewset 含有 post,那么你需要重载 create 方法,如果含有 patch,那么就需要重载 update 方法。

# 假设现在是个博客,有一个创建文章,与修改文章的功能, model为Article。
class ArticleSerializer(serializers.Serializer):
    user = serializers.HiddenField(
        default=serializers.CurrentUserDefault())
    name = serializers.CharField(max_length=20)
    content = serializers.CharField()

    def create(self, validated_data):
    # 除了用户,其他数据可以从validated_data这个字典中获取
    # 注意,users在这里是放在上下文中的request,而不是直接的request
        user = self.context['request'].user
        name = validated_data['name ']
        content = validated_data['content ']
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):
    # 更新的特别之处在于你已经获取到了这个对象instance
        instance.name = validated_data.get('name')
        instance.content = validated_data.get('content')
        instance.save()
        return instance

可能会有人好奇,系统是怎么知道,我们需要调用serializercreate方法,还是 update 方法,我们从 save() 方法可以看出,判断的依据是:

if self.instance is not None:
    pass

那么我们的mixins的create与update也已经在为开发者设置好了,

# CreateModelMixin
serializer = self.get_serializer(data=request.data)
# UpdateModelMixin
serializer = self.get_serializer(instance, data=request.data, partial=partial)

也就是说,在update通过get_object( )的方法获取到了instance,然后传递给serializerserializer再根据是否有传递instance来判断来调用哪个方法!

# 三、ModelSerializer

讲了很多 Serializer 的,在这个时候,我还是强烈建议使用 ModelSerializer,因为在大多数情况下,我们都是基于model字段去开发。

# 好处:

ModelSerializer 已经重载了 createupdate 方法,它能够满足将post或patch上来的数据进行进行直接地创建与更新,除非有额外需求,那么就可以重载create与update方法。

ModelSerializerMeta 中设置 fields 字段,系统会自动进行映射,省去每个字段再写一个 field

class UserDetailSerializer(serializers.ModelSerializer):
    """
    用户详情序列化
    """

    class Meta:
        model = User
        fields = ("name", "gender", "birthday", "email", "mobile")
        # fields = '__all__': 表示所有字段
        # exclude = ('add_time',):  除去指定的某些字段
        # 这三种方式,存在一个即可

# ModelSerializer需要解決的2個問題:

    1. 某个字段不属于指定 model,它是 write_only,需要用户传进来,但我们不能对它进行 save(),因为 ModelSerializer 是基于Model,这个字段在 Model中没有对应,这个时候, 我们需要重载 validate, 如在用户注册时,我们需要填写验证码,这个验证码只需要验证,不需要保存到用户这个Model中:
    def validate(self, attrs):
        del attrs["code"]
        return attrs
    
    1. 某个字段不属于指定 model,它是 read_only,只需要将它序列化传递给用户,但是在这个model中,没有这个字段!我们需要用到 SerializerMethodField, 假设需要返回用户加入这个网站多久了,不可能维持这样加入的天数这样一个数据,一般会记录用户加入的时间点,然后当用户获取这个数据,我们再计算返回给它。
    class UserSerializer(serializers.ModelSerializer):  
        days_since_joined = serializers.SerializerMethodField()
        # 方法写法:get_ + 字段
        def get_days_since_joined(self, obj):
        # obj指这个model的对象
            return (now() - obj.date_joined).days
    
        class Meta:
            model = User
    

当然,这个的SerializerMethodField用法还相对简单一点,后面还会有比较复杂的情况。

# 四、关于外键的serializers

其实,外键的field也比较简单,如果我们直接使用 serializers.Serializer,那么直接用 PrimaryKeyRelatedField 就解决了

假设现在有一门课 python入门教学(course),它的类别是 python(catogory)。

# 指定queryset
category = serializers.PrimaryKeyRelatedField(queryset=CourseCategory.objects.all(), required=True)

ModelSerializer就更简单了,直接通过映射就好了

不过这样只是用户获得的只是一个外键类别的id,并不能获取到详细的信息,如果想要获取到具体信息,那需要嵌套serializer

category = CourseCategorySerializer()

注意:上面两种方式,外键都是正向取得,下面介绍怎么反向去取,如,我们需要获取python这个类别下,有什么课程

首先,在课程coursemodel中,需要在外键中设置related_name

class Course(model.Model):
    category = models.ForeignKey(CourseCategory, related_name='courses')
# 反向取课程,通过related_name
# 一对多,一个类别下有多个课程,一定要设定many=True
courses = CourseSerializer(many=True)

还有一个小问题:我们在上面提到ModelSerializer需要解决的第二个问题中,其实还有一种情况,就是某个字段属于指定model,但不能获取到相关数据

假设现在是一个多级分类的课程,例如,编程语言 –> python –> python入门学习课程,编程语言与python属于类别,另外一个属于课程,编程语言类别是python类别的一个外键,而且属于同一个model,实现方法:

parent_category = models.ForeignKey('self', null=True, blank=True, 
                    verbose_name='父类目别',
                    related_name='sub_cat')

现在获取编程语言下的课程,显然无法直接获取到python入门学习这个课程,因为它们两没有外键关系。SerializerMethodField()也可以解决这个问题,只要在自定义的方法中实现相关的逻辑即可!

courses = SerializerMethodField()
def get_courses(self, obj):
    all_courses = Course.objects.filter(category__parent_category_id=obj.id)
    courses_serializer = CourseSerializer(all_course, many=True, 
                    context={'request': self.context['request']})
    return courses_serializer.data

上面的例子看起来有点奇怪,因为我们在 SerializerMethodField() 嵌套了 serializer,就需要自己进行序列化,然后再从data就可以取出json数据。

可以看到传递的参数是分别是:querysetmany=True多个对象context上下文

这个context十分关键,如果不将request传递给它,在序列化的时候,图片与文件这些Field不会再前面加上域名,也就是说,只会有/media/img…这样的路径!

# 原文

https://blog.csdn.net/l_vip/article/details/79156113

上次编辑于: 6/17/2021, 3:08:08 AM