DRF 的一些实践 Part1: Serializer
SLZ 只做输入/输出相关的逻辑, 尽量避免处理具体的业务逻辑
命名建议
- 写全
xxSerializers
- 也可以用缩写
xxSLZ
(注意全大写, 不要用xxSlz
)
两种风格
1. 全部serializer放在一起
同一个application的全部serializer放在一起
application
|- serializers.py
如果都放在一起, 建议区分 input
和 output
class LoginLogInputSLZ(serializers.Serializer):
pass
class LoginLogOutputSLZ(serializers.Serializer):
pass
class LoginLogListApi(generics.ListAPIView):
serializer_class = LoginLogOutputSLZ
2. 直接放在view中
放在api定义中(Serializer should be nested in the API and be named either InputSerializer or OutputSerializer
)
class UserListApi(APIView):
class OutputSerializer(serializers.Serializer):
pass
class InputSerializer(serializers.Serializer):
pass
def get(self, request):
users = user_list()
data = self.OutputSerializer(users, many=True).data
return Response(data)
InputSLZ
只做校验相关的逻辑, 避免放入业务逻辑
1. method: validate_{field}
校验某个字段: validate_{field}
from rest_framework import serializers
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
content = serializers.CharField()
def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value
2. method: validate
整体校验(对象级别): validate
from rest_framework import serializers
class EventSerializer(serializers.Serializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()
def validate(self, data):
"""
Check that the start is before the stop.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data
3. 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])
...
4. to_internal_value
官方文档: Overriding serialization and deserialization behavior
如果输入的表单和业务模型数据结构差异很大, 需要做全局转换, 可以使用 to_internal_value
统一处理
class DynamicFieldUpdateInputSLZ(serializers.Serializer):
def to_internal_value(self, data):
field_a = data.get("field_a")
# do something with field_a
return {
"x": field_a
}
OutputSLZ
只做展示拼装相关的逻辑
1. SerializerMethodField
使用 SerializerMethodField
做一些转换/格式化/映射之类的逻辑
class LoginLogOutputSLZ(serializers.Serializer):
datetime = serializers.SerializerMethodField(help_text=_("登录时间"), required=False)
def get_datetime(self, obj):
return obj.create_time
注意: 尽量避免在 SerializerMethodField
或者model
的@property
中做一些独立的查询工作, 这样会带来查询放大, 一次请求查 1000 条数据, 可能会有 1001 次数据库查询
class Profile(models.Model):
@property
def last_login_time(self) -> Optional[datetime.datetime]:
"""获取用户最近一次登录时间"""
latest_login = self.login_set.filter(is_success=True).latest()
if latest_login:
return latest_login.create_time
return None
如果在多个SLZ中反复出现同一个SerializerMethodField
- 处理逻辑跟
展示层
关系不大, 可下沉到model
的@property
- 处理逻辑跟
展示层
有关系, 可以抽象成mixin
2. to_representation
官方文档: Overriding serialization and deserialization behavior
如果需要做较大的转换, 例如得到的模型数据相对返回的数据结构差异很大, 需要做大量的转换/映射/内嵌/判断等, 此时可以直接定义to_representation(self, obj)
简化
class GeneralLogOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text=_("ID"))
extra_value = serializers.JSONField(help_text=_("额外信息"))
def to_representation(self, obj):
instance = super().to_representation(obj)
extra_value = instance["extra_value"]
field_a = extra_value.get("field_a")
return {
"field_a": field_a,
"filed_b": field_b,
}
3. serializer_context
通过get_serializer_context
设置context变量, 之后在渲染时可以通过self.context.get("{KEY}")
获取
class LoginLogListApi(generics.ListAPIView):
serializer_class = LoginLogOutputSLZ
def get_serializer_context(self):
# request/format/view
ctx = super().get_serializer_context(self)
ctx["category_name_map"] = get_category_display_name_map()
return ctx
class LoginLogOutputSLZ(serializers.Serializer):
category_display_name = serializers.SerializerMethodField(help_text=_("所属目录"), required=False)
def get_category_display_name(self, obj) -> str:
"""get category display name from log extra value"""
category_id = obj.profile.id
category_name_map = self.context.get("category_name_map")
category_display_name = category_name_map.get(category_id, PLACE_HOLDER)
return category_display_name
坑: 如果自己实例化slz, 需要显式传递context, 否则在slz中是拿不到的
context = self.get_serializer_context()
xxOutputSLZ(results, many=True, context=context)
SLZ 传递的参数
- input slz 可能传递一些查询相关的参数, 例如
order_by=create_time
- output slz可能传递一些返回结果相关的参数, 例如
fields=a,b,c
逻辑上是需要支持的, 但是, 要严格控制字段白名单
例如, order_by
只允许传递 主键或有建立相关索引的字段名, 而不是任意字段, 避免 order_by=enabled
这类查询无法命中索引带来的慢查询
同样, fileds
应该避免允许传递重
的property字段(存在额外查询). 如果调用方不传递, 应该默认使用最小化
集合, 而不是全部
;
ModelSerializer 尽量使用最小化集合
如果使用了serializers.ModelSerializer
, Meta.fields
尽量使用枚举, 或者配置exclude
, 而不是fields = "__all__"
class CategorySettingOutputSLZ(serializers.ModelSerializer):
class Meta:
model = Setting
fields = ["key", "namespace", "region", "value", "enabled"]
# 尽量避免
# fields = "__all__"
# 可以用这个, 但是后续加新字段可能会忘记加exclude
# exclude = ("create_time", "update_time")
避免:
- 暴露不必要的字段出去, (场景: 未来字段变更想去掉, 如果原来没有暴露, 那么直接去, 否则需要找调用方逐一确认)
- 不小心暴露不能更新的字段出去, 例如
create_time
/update_time
单一职责
单一职责原则: 一个slz只给一个view使用, 尽量避免多个view共用一个slz
不一定需要继承: 复用 vs 不复用
极端处理: 每个 slz 都是独立的, 及时存在公共字段, 也不抽父类出来;
带来的后果: 字段的变更/校验逻辑变更等, 需要改动涉及的所有 slz
使用继承: 变更只需要改一个地方, 但是存在风险, 改一个地方可能动到意想不到的其他接口校验/返回
倾向的做法:
- 刚开始开发, 完全不考虑继承, 完全独立
- 主体逻辑和接口开发完之后, 可以根据需要加入部分继承, 例如公共的
create_time/update_time
等字段 - 如果使用继承, 尽量减少继承的层级(不大于 3 层), 并且需要考虑
隔离
: 输入/输出的 slz 父类进行隔离, 或者核心模块slz和非核心模块slz隔离, 即使它们有相同的字段;(缩小未来变更的影响范围)