























SLZ 只做输入/输出相关的逻辑, 尽量避免处理具体的业务逻辑
xxSerializersxxSLZ(注意全大写, 不要用xxSlz)同一个application的全部serializer放在一起
application
|- serializers.py
如果都放在一起, 建议区分 input 和 output
class LoginLogInputSLZ(serializers.Serializer):
pass
class LoginLogOutputSLZ(serializers.Serializer):
pass
class LoginLogListApi(generics.ListAPIView):
serializer_class = LoginLogOutputSLZ
放在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)
只做校验相关的逻辑, 避免放入业务逻辑
校验某个字段: 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
整体校验(对象级别): 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
复用 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])
...
官方文档: 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
}
只做展示拼装相关的逻辑
使用 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官方文档: 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,
}
通过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)
order_by=create_timefields=a,b,c逻辑上是需要支持的, 但是, 要严格控制字段白名单
例如, order_by只允许传递 主键或有建立相关索引的字段名, 而不是任意字段, 避免 order_by=enabled这类查询无法命中索引带来的慢查询
同样, fileds应该避免允许传递重的property字段(存在额外查询). 如果调用方不传递, 应该默认使用最小化集合, 而不是全部;
如果使用了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
极端处理: 每个 slz 都是独立的, 及时存在公共字段, 也不抽父类出来;
带来的后果: 字段的变更/校验逻辑变更等, 需要改动涉及的所有 slz
使用继承: 变更只需要改一个地方, 但是存在风险, 改一个地方可能动到意想不到的其他接口校验/返回
倾向的做法:
create_time/update_time等字段隔离: 输入/输出的 slz 父类进行隔离, 或者核心模块slz和非核心模块slz隔离, 即使它们有相同的字段;(缩小未来变更的影响范围)此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。