python

django rest framework serializers小结

文章暂存

systemime
2021-06-03
15 min

摘要.

# 引言

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 有以下几个作用:
  - 将 queryset 与 model 实例等进行序列化,转化成 json 格式,返回给用户 (api 接口)。
  - 将 post 与 patch/put 的上来的数据进行验证。
  - 对 post 与 patch/put 数据进行处理。

(后面的内容,将用 patch 表示 put/patch 更新,博主认为 patch 更贴近更新的说法)
  简单来说,针对 get 来说,serializers 的作用体现在第一条,但如果是其他请求,serializers 能够发挥 2,3 条的作用!

本文比较长,用张图简单介绍一下主要内容

# serializers.fieild

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

# 1. 常用的 field

CharField、BooleanField、IntegerField、DateTimeField 这几个用得比较多,我们把外键的 field 放到后面去说!

# 举例子
mobile = serializers.CharField(max_length=11, min_length=11)
age = serializers.IntegerField(min_value=1, max_value=100)
# format可以设置时间的格式,下面例子会输出如:2018-1-24 12:10
pay_time = serializers.DateTimeField(read_only=True,format='%Y-%m-%d %H:%M')
is_hot = serializers.BooleanField()
复制代码

不同的是,我们在 django 中,form 更强调对提交的表单进行一种验证,而 serializer 的 field 不仅在进行数据验证时起着至关重要的作用,在将数据进行序列化后返回也发挥着重要作用!!
  我们可以看出,不同的 field 可以用不同的关键参数,除此之外,还有一些十分重要有用的参数。

# 2. Core arguments 参数

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

order_sn = serializers.CharField(readonly=True)
复制代码

write_only: 与 read_only 对应 required: 顾名思义,就是这个字段是否必填。
allow_null/allow_blank:是否允许为 NULL / 空 。 error_messages:出错时,信息提示。

name = serializers.CharField(required=True, min_length=6,
                error_messages={
                    'min_length': '名字不能小于6个字符',
                    'required': '请填写名字'})
                                    
复制代码

label: 字段显示设置,如 label='验证码' help_text: 在指定字段增加一些提示文字,这两个字段作用于 api 页面比较有用 style: 说明字段的类型,这样看可能比较抽象,看下面例子:

# 在api页面,输入密码就会以*显示
password = serializers.CharField(
    style={'input_type': 'password'})
# 会显示选项框
color_channel = serializers.ChoiceField(
    choices=['red', 'green', 'blue'],
    style={'base_template': 'radio.html'})
复制代码

这里面,还有一个十分有用的 validators 参数,这个我们会在后面提及!

# 3. HiddenField

HiddenField 的值不依靠输入,而需要设置默认的值,不需要用户自己 post 数据过来,也不会显式返回给用户,最常用的就是 user!!
  我们在登录情况下,进行一些操作,假设一个用户去收藏了某一门课,那么后台应该自动识别这个用户,然后用户只需要将课程的 id post 过来,那么这样的功能,我们配合 CurrentUserDefault() 实现。

# 这样就可以直接获取到当前用户
user = serializers.HiddenField(
	default=serializers.CurrentUserDefault())
复制代码

# save instance

这个标题是官方文档的一个小标题,我觉得用的很好,一眼看出,这是为 post 和 patch 所设置的,没错,这一部分功能是专门为这两种请求所设计的,如果只是简单的 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 的操作,它去调用了 serializer 的 create 或 update 方法,不是 mixins 中的!!!我们看一下流程图(以 post 为例)

讲了那么多,我们到底需要干什么!重载这两个方法!!
  如果你的 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
复制代码

可能会有人好奇,系统是怎么知道,我们需要调用 serializer 的 create 方法,还是 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,然后传递给 serializer,serializer 再根据是否有传递 instance 来判断来调用哪个方法!

# Validation 自定义验证逻辑

# 单独的 validate

我们在上面提到 field,它能起到一定的验证作用,但很明显,它存在很大的局限性,举个简单的例子,我们要判断我们手机号码,如果使用 CharField(max_length=11, min_length=11),它只能确保我们输入的是 11 个字符,那么我们需要自定义!

mobile_phone = serializers.CharField(max_length=11, min_length=11)

def validate_mobile_phone(self, mobile_phone):
	# 注意参数,self以及字段名
	# 注意函数名写法,validate_ + 字段名字
	if not re.match(REGEX_MOBILE, mobile):
	# REGEX_MOBILE表示手机的正则表达式
		raise serializers.ValidationError("手机号码非法")
	return mobile_phone
复制代码

当然,这里面还可以加入很多逻辑,例如,还可以判断手机是否原本就存在数据库等等。

# 联合 validate

上面验证方式,只能验证一个字段,如果是两个字段联合在一起进行验证,那么我们就可以重载 validate( ) 方法。

    start = serializers.DateTimeField()
    finish = serializers.DateTimeField()
    
    def validate(self, attrs):
    # 传进来什么参数,就返回什么参数,一般情况下用attrs
        if data['start'] > data['finish']:
            raise serializers.ValidationError("finish must occur after start")
        return attrs
复制代码

这个方法非常的有用,我们还可以再这里对一些 read_only 的字段进行操作,我们在 read_only 提及到一个例子,订单号的生成,我们可以在这步生成一个订单号,然后添加到 attrs 这个字典中。

order_sn = serializers.CharField(readonly=True)
def validate(self, attrs):
	# 调用一个方法生成order_sn
	attrs['order_sn'] = generate_order_sn()
	return attrs
复制代码

这个方法运用在 modelserializer 中,可以剔除掉 write_only 的字段,这个字段只验证,但不存在与指定的 model 当中,即不能 save( ),可以在这 delete 掉!

# Validators

validators 可以直接作用于某个字段,这个时候,它与单独的 validate 作用差不多

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

class GameRecord(serializers.Serializer):
    score = 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 Meta:
        validators = [
            UniqueTogetherValidator(
                queryset=UserFav.objects.all(),
                fields=('user', 'course'),
                message='已经收藏'
            )]
复制代码

# ModelSerializer

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

# 好处:

ModelSerializer 已经重载了 create 与 update 方法,它能够满足将 post 或 patch 上来的数据进行进行直接地创建与更新,除非有额外需求,那么就可以重载 create 与 update 方法。   ModelSerializer 在 Meta 中设置 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
复制代码

2,某个字段不属于指定 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 这个类别下,有什么课程。   首先,在课程 course 的 model 中,需要在外键中设置 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 数据。
  可以看到传递的参数是分别是:queryset,many=True 多个对象,context 上下文。这个 context 十分关键,如果不将 request 传递给它,在序列化的时候,图片与文件这些 Field 不会再前面加上域名,也就是说,只会有 / media/img... 这样的路径!

# 后言

写到这,serializers 的小结就写完啦,如果错误的地方,欢迎各位的指出!接下来会继续写 drf 的相关文章~~

CSDN: http://blog.csdn.net/l\_vip/article/details/79156113 https://juejin.cn/post/6844903555321577486 https://juejin.cn/post/6844903555321577486

上次编辑于: 6/5/2021, 11:41:57 AM