Django Rest Framework 序列号组件

serializers是什么?

将复杂的数据结构,例如ORM中的QuerySet或者Model实例对象转换成Python内置的数据类型,从而进一步方便数据和json,xml等格式的数据进行交互。

serializers的作用

1.将queryset与model实例等进行序列化,转化成json格式,返回给用户(api接口)。
2.将post与patch/put的上来的数据进行验证。
3.对post与patch/put数据进行处理。(后面的内容,将用patch表示put/patch更新,博主认为patch更贴近更新的说法)

简单来说,针对get来说,serializers的作用体现在第一条

但如果是其他请求,serializers能够发挥2,3条的作用!

常用的field

1
2
3
4
5
mobile = serializers.CharField(max_length=11, min_length=11)
age = serializers.IntegerField(min_value=1, max_value=100)
# format可以设置时间的格式,下面例子会输出如:2018-3-20 12:10
pay_time = serializers.DateTimeField(read_only=True,format='%Y-%m-%d %H:%M')
is_hot = serializers.BooleanField() # 设置商品是否热销

HiddenField

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

我们在收藏某件商品的时候,后台要获取到当前的用户;相当于前台只需要传递过来一个商品的ID即可;那么在后台我根据当前的登入用户和当前的商品ID即可判断用户是否收藏过该商品;这就是一个联合唯一主键的判断;这同样需要使用HiddenField

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 如果你的viewset含有post,那么你需要重载create方法,如果含有patch,那么就需要重载update方法

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

字段参数

read_only:True表示不允许用户自己上传,只能用于api的输出。如果某个字段设置了read_only=True,那么就不需要进行数据验证,只会在返回时,将这个字段序列化后返回

在用户进行购物的时候,用户post订单时,肯定会产生一个订单号,而这个订单号应该由后台逻辑完成,而不应该由用户post过来,如果不设置read_only=True,那么验证的时候就会报错。

我们在网上购物时,支付时通常会产生支付状态,交易号,订单号,支付时间等字段,这些字段都应该设置为read_only=True,即这些字段都应该由后台产生然后返回给客户端;

在用户提交订单的时候,我们在这里给用户新增一个字段,就是支付宝支付的URL
要设置为read_only=True,这样的话,就不能让用户端提交了,而是服务器端生成
返回给用户的

1
alipay_url = serializers.SerializerMethodField(read_only=True)

write_only: 与read_only对应;就是用户post过来的数据,后台服务器处理后不会再经过序列化后返回给客户端

最常见的就是我们在使用手机注册的验证码和填写的密码。

reuqired: 是否必填

allow_null/allow_blank: 是否允许为NULL/空

error_message: 出错时的提示信息

label: 字段显示

Help_text: 在指定字段增加一些提示文字,这两个字段作用于api页面比较有用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 示例
code = serializers.CharField(required=True, write_only=True, label="验证码", max_length=6, min_length=6,
error_messages={
"blank": "请输入验证码",
"required": "请输入验证码",
"max_length": "验证码格式错误",
"min_length": "验证码格式错误"
},
help_text="验证码")

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

# 同理我们也应该将password设置write_only参数,不让它返回回去,这样容易被截获
password = serializers.CharField(
style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True)

color_channel = serializers.ChoiceField(
choices=['red', 'green', 'blue'],
style={'base_template': 'radio.html'})

Validation自定义验证逻辑

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

拿实际生产环境下的例子来说,光针对用户输入的手机号码,我们在后端就需要进行验证

例如该手机号码是否注册,手机号码是否合法(例如满足手机号码的正则表达式,以及要验证该手机号码向后台发送请求验证短信的频率等等

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
class SmsSerializer(serializers.Serializer):
"""
为什不用ModelSerializer来完成手机号码的验证呢?
因为VerifyCode中还有一个字段是手机验证码字段,直接使用ModelSerializer来验证会报错
因为ModelSerializer会自动生成VerifyCode模型中的所有字段;但是
我们传递过来的就是一个手机号,判断它是否是合法的
因此使用Serializer来自定义合法性校验规则,相当于就是钩子函数
单独对手机号码进行验证
"""

mobile = serializers.CharField(max_length=11)

# 使用validate_字段名(self, 字段名):要么返回字段,要么抛出异常
def validate_mobile(self, mobile):
"""
验证手机号码,记住这里的validate_字段名一定要是数据模型中的字段
:param attrs:
:return:
"""
# 手机号码是否注册,查询User表即可
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

订单号的生成,我们可以在这步生成一个订单号,然后添加到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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class OrderSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault())

pay_status = serializers.CharField(read_only=True)
trade_no = serializers.CharField(read_only=True)
order_sn = serializers.CharField(read_only=True)
pay_time = serializers.DateTimeField(read_only=True)

"""
在用户提交订单的时候,我们在这里给用户新增一个字段,就是支付宝支付的URL
要设置为read_only=True,这样的话,就不能让用户端提交了,而是服务器端生成
返回给用户的
"""

alipay_url = serializers.SerializerMethodField(read_only=True)

def get_alipay_url(self, obj): # obj就是OrderSerializer对象
alipay = AliPay(
appid="2019081200490227", # 沙箱环境中可以找到
app_notify_url="http://47.92.87.172:8000/alipay/return/",
app_private_key_path=private_key_path, # 个人私钥
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,上线的时候修改为False即可
return_url="http://47.92.87.172:8000/alipay/return/"
)

url = alipay.direct_pay(
# 一个订单里面可能有多个商品,因此subject
# 不适合使用商品名称
subject=obj.order_sn,
out_trade_no=obj.order_sn,
total_amount=obj.order_mount,
)
re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)

return re_url

def generate_order_sn(self):

"""
后台系统生成订单号
这个是系统后台生成的:
当前时间+userId+随机数
"""

random_ins = Random()
order_sn = "{time_str}{user_id}{random_num}".format(time_str=time.strftime("%Y%m%d%H%M%S"),
user_id=self.context["request"].user.id,
random_num=random_ins.randint(1000, 9999))
return order_sn

def validate(self, attrs): # 全局钩子函数
attrs["order_sn"] = self.generate_order_sn()
return attrs

class Meta:
model = OrderInfo
fields = "__all__"

#这个方法运用在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: 指定某一个对象是唯一的,如,用户名只能存在唯一:

UniqueTogetherValidator: 联合唯一,例如我们需要判断用户是否收藏了某个商品,前端只需要传递过来一个商品ID即可。这个时候就不是像上面那样单独作用于某个字段,而是需要进行联合唯一的判断,即用户ID和商品ID;此时我们需要在Meta中设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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字段去开发。

Why:

ModelSerializer已经重载了create与update方法,它能够满足将post或patch上来的数据进行进行直接地创建与更新,除非有额外需求,那么就可以重载create与update方法。
ModelSerializer在Meta中设置fields字段,系统会自动进行映射,省去每个字段再写一个field。

1
2
3
4
5
6
7
8
9
10
class UserDetailSerializer(serializers.ModelSerializer):
"""
用户详情序列化
"""
class Meta:
model = User
fields = ("name", "gender", "birthday", "email", "mobile")
# fields = '__all__': 表示所有字段
# exclude = ('add_time',): 除去指定的某些字段
# 这三种方式,存在一个即可

ModelSerializer需要解决的2个问题

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

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

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

某个字段不属于指定model,它是read_only,只需要将它序列化传递给用户,但是在这个model中,没有这个字段!我们需要用到SerializerMethodField。

假设需要返回用户加入这个网站多久了,不可能维持这样加入的天数这样一个数据,一般会记录用户加入的时间点,然后当用户获取这个数据,我们再计算返回给它。

1
2
3
4
5
6
7
8
9
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

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

例如,编程语言–>python–>python入门学习课程,编程语言与python属于类别,另外一个属于课程,编程语言类别是python类别的一个外键,而且属于同一个model

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

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

1
2
3
4
5
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

关于外键的serializers

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

以下两种都是外键正向获取

1
2
3
category = serializers.PrimaryKeyRelatedField(queryset=CourseCategory.objects.all(), required=True)
# 使用 ModelSerializer类 可以直接映射
category = CourseCategorySerializer()

外键反向获取

1
2
3
class Course(model.Model):
# 外键设置related_name
category = models.ForeignKey(CourseCategory, related_name='courses')

反向获取课程,需要通过related_name

一对多,一个类别下有多个课程,一定要设定many=True

1
courses = CourseSerializer(many=True)