DRF 之序列化小节

2018 年 10 月 22 日 • 阅读数: 94

DRF 之序列化小结

前言

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. “翻译出来就是,将复杂的数据结构,例如ORM中的QuerySet或者Model实例对象转换成Python内置的数据类型,从而进一步方便数据和json,xml等格式的数据进行交互

根据实际的工作经验,我来总结下serializers的作用:

  • querysetmodel 实例等进行序列化,转化成json格式,返回给用户(api接口)
  • postpatch/put 的上来的数据进行验证
  • postpatch/put 数据进行处理(后面的内容,将用 patch 表示 put/patch 更新)
  • 针对 get 来说,serializers的作用体现在第一条,但如果是其他请求,serializers能够发挥2,3条的作用

用一张表格来说明下它的作用如下

主要内容 小标题 Serializer ModelSerializer
field 常用field CharField、BooleanField、IntegerField、DateTimeField
Core arguments read_only、write_only、required、label、help_text、style
HiddenField HiddenField的值不需要用户自己post数据过来,也不会返回给数据
save instance save方法 serializer.save()它会调用serializer的create或update方法
create方法 当有post请求,需要重写该方法 ModelSerializer已经封装了这两个方法,如有需要可自行重载
update方法 当有patch请求,需要重写该方法
Validation自定义验证逻辑 单独的validate 对某个字段进行自定义验证逻辑,重载validate_+字段名
联合validate 重写validate方法,对多个字段进行验证
Validators 单独作用于某个field:作用与重载validate_+字段名相似
UniqueValidator:指定某一个对象是唯一的,直接作用于某个field
UniqueTogetherValidator:联合唯一,需要在Meta属性中设置
ModelSerializer专属 validate 可以实现删除用户提交的字段,但该字段不存在指定的Model,避免save()出错
Serializer MethodField 将Model不存在的字段或者获取不到的数据序列化返回给用户
外键的serializers 正向 PrimaryKeyRelatedField:不关心外键具体内容 通过field已经映射
嵌套serializer,获取外键具体内容
反向 Model设置related_name,通过related_name嵌套serializer

Field

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

常用的Field

CharField、BooleanField、IntegerField、DateTimeField这几个用得比较多,通过名字就可以大致知道它们的含义,具体的使用方式可以参考一下下面的例子:

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()

Core argumens

我们可以给字段指定一些特殊参数,来限制它的一些输出表现

参数 说明
read_only 为True表示不允许用户自己上传,只能用于api的输出
write_only 为True表示用户post过来的数据,后台服务器处理后不会再返回给客户端
required 为True表示该字段必填
allow_null 为True表示允许为空(一般和 allow_blank 配合使用)
allow_blank 为True表示允许为空(一般和 allow_null 配合使用)
error_messages 出错时信息提示,常用的如:blank、required、max_length、min_length
label 字段显示设置,如 label='密码'
help_text 在指定字段增加一些提示文字,如 help_text='请输入密码'
style 说明字段的类型,如 style={'input_type': 'password'}

HiddenField

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

我们在登录情况下,进行一些操作,假设一个用户去收藏了某一门课,那么后台应该自动识别这个用户,然后用户只需要将课程的id传递过来,那么这样的功能,我们可以配合 CurrentUserDefault() 实现

# 这样就可以直接获取到当前用户,前提是用户经过了身份验证
user = serializers.HiddenField(default=serializers.CurrentUserDefault())

Sava Instance

save方法

当我们数据经过验证之后,我们可以调用 Serializer.save() 方法,这个方法可以让我们保存实例到数据库,当然这个方法是为post和patch设置的,如果只是简单的get请求,那么在设置了前面的field可能就能够满足需求

要想知道 Serializer.save() 的原理可以参考一下其源码

def save(self, **kwargs):
        assert not hasattr(self, 'save_object'), (
            'Serializer `%s.%s` has old-style version 2 `.save_object()` '
            'that is no longer compatible with REST framework 3. '
            'Use the new-style `.create()` and `.update()` methods instead.' %
            (self.__class__.__module__, self.__class__.__name__)
        )

        assert hasattr(self, '_errors'), (
            'You must call `.is_valid()` before calling `.save()`.'
        )

        assert not self.errors, (
            'You cannot call `.save()` on a serializer with invalid data.'
        )

        # Guard against incorrect use of `serializer.save(commit=False)`
        assert 'commit' not in kwargs, (
            "'commit' is not a valid keyword argument to the 'save()' method. "
            "If you need to access data before committing to the database then "
            "inspect 'serializer.validated_data' instead. "
            "You can also pass additional keyword arguments to 'save()' if you "
            "need to set extra attributes on the saved model instance. "
            "For example: 'serializer.save(owner=request.user)'.'"
        )

        assert not hasattr(self, '_data'), (
            "You cannot call `.save()` after accessing `serializer.data`."
            "If you need to access data before committing to the database then "
            "inspect 'serializer.validated_data' instead. "
        )

        validated_data = dict(
            list(self.validated_data.items()) +
            list(kwargs.items())
        )

        if self.instance is not None:
            self.instance = self.update(self.instance, validated_data)
            assert self.instance is not None, (
                '`update()` did not return an object instance.'
            )
        else:
            self.instance = self.create(validated_data)
            assert self.instance is not None, (
                '`create()` did not return an object instance.'
            )

        return self.instance

显然,serializer.save()的操作,它去调用了serializer的 create()update() 方法

post请求的流程如下图,同理patch请求也类似

1537761256731

create方法

所以如果我们的ViewSet含有post,并且想要保存实例到数据库,就需要重载 create 方法

update方法

如果我们的ViewSet含有patch或者put方法,并且我们需要更新数据,就需要重载 update() 方法

那么系统是怎么知道,我们需要调用serializer的create方法,还是update方法,我们从 save() 方法可以看出,判断的依据是

if self.instance is not None:
	pass

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

instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)

Validation

单独的validate

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

举个实际生产环境下的例子,针对用户输入的手机号码,我们在后端需要进行验证,例如该手机号码是否注册,手机号码是否合法,以及要验证该手机号码向后台发送请求验证短信的频率等等

class SmsSerializer(serializers.Serializer):
    """
    为什不用ModelSerializer来完成手机号码的验证呢?
    因为VerifyCode中还有一个字段是手机验证码字段,直接使用ModelSerializer来验证会报错
    因为ModelSerializer会自动生成VerifyCode模型中的所有字段;但是
    我们传递过来的就是一个手机号,判断它是否是合法的
    因此使用Serializer来自定义合法性校验规则,相当于就是钩子函数
    单独对手机号码进行验证
    """
    mobile = serializers.CharField(max_length=11)
  # 使用validate_字段名(self, 字段名):要么返回字段,要么抛出异常
    def validate_mobile(self, mobile):
        """
        验证手机号码,记住这里的validate_字段名一定要是数据模型中的字段
        """
        # 手机号码是否注册,查询UserProfile表即可
        if User.objects.filter(mobile=mobile).exists():
            raise serializers.ValidationError("用户已经存在")

        # 验证手机号码是否合法,这部分应该是在前端做的,当然后台也需要进行验证
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("手机号码格式不正确")

        # 验证发送频率,如果不做,用户可以一直向后台发送,请求验证码;
        # 会造成很大的压力,限制一分钟只能发送一次 7-8 09:00
        one_minute_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        """
        如果添加时间在一分钟以内,它肯定是大于你一分钟之前的时间的
        如果这条记录存在
        """
        if VerifyCode.objects.filter(add_time__gt=one_minute_ago, mobile=mobile):
            raise serializers.ValidationError("抱歉,一分钟只能发送一次")
        # 如果验证通过,我就将这个mobile返回去,这里一定要有一个返回
        return mobile

联合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的字段进行操作

这个方法运用在modelserializer中,可以剔除掉write_only的字段,这个字段只验证,但不存在于指定的model当中,即不能save( ),可以在这delete掉;例如短信验证码验证完毕后就可以删除了

def validate(self, attrs):
        """
        判断完毕后删除验证码,因为没有什么用了
        """
        attrs["mobile"] = attrs["username"]
        del attrs["code"]
        return attrs

Validators

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

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

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

username = serializers.CharField(required=True, allow_blank=False, label="用户名", max_length=16, min_length=6,validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

UniqueTogetherValidator: 联合唯一,例如我们需要判断用户是否收藏了某个商品,前端只需要传递过来一个商品ID即可

这个时候就不是像上面那样单独作用于某个字段,而是需要进行联合唯一的判断,即用户ID和商品ID;此时我们需要在Meta中设置

class UserFavSerializer(serializers.ModelSerializer):
    # 获取到当前用户
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = UserFav
        """
        我们需要获取的是当前登入的user
        以后要取消收藏,只需要获取这里的id即可
        UniqueTogetherValidator作用在多个字段之上
        因为是联合唯一主键
        """
        validators = [
            UniqueTogetherValidator(
                queryset=UserFav.objects.all(),
                fields=('user', 'goods'),
                message="已经收藏"
            )
        ]
        fields = ("user", "goods", "id")

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',):  除去指定的某些字段
        # 这三种方式,存在一个即可

Validate

某个字段不属于指定model,它是write_only,需要用户传进来,但我们不能对它进行save( ),因为ModelSerializer是基于Model,这个字段在Model中没有对应,这个时候,我们需要重载validate

如在用户注册时,我们需要填写验证码,这个验证码只需要验证,不需要保存到用户这个Model中

def validate(self, attrs):
        del attrs["code"]
        return attrs

SerializerMethodField

某个字段不属于指定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
        field = '__all__'

当然,这个的SerializerMethodField用法还相对简单一点,后面还会有比较复杂的情况,如嵌套序列化

外键的serializer

正向

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

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

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

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

category = CourseCategorySerializer()

反向

在一对多关系中,我们在一的一端要获取多的一端的数据可以通过 related_name

标签: DRF序列化API

召唤伊斯特瓦尔