惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
Jina AI
Jina AI
MyScale Blog
MyScale Blog
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
F
Fortinet All Blogs
F
Full Disclosure
M
MIT News - Artificial intelligence
博客园 - 三生石上(FineUI控件)
P
Proofpoint News Feed
J
Java Code Geeks
I
InfoQ
小众软件
小众软件
B
Blog
U
Unit 42
MongoDB | Blog
MongoDB | Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
V
V2EX
Microsoft Azure Blog
Microsoft Azure Blog
G
Google Developers Blog
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
Stack Overflow Blog
Stack Overflow Blog
GbyAI
GbyAI
A
About on SuperTechFans
Y
Y Combinator Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
LangChain Blog
The Last Watchdog
The Last Watchdog
C
Cybersecurity and Infrastructure Security Agency CISA
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Heimdal Security Blog
Recent Announcements
Recent Announcements
L
LINUX DO - 热门话题
P
Privacy International News Feed
阮一峰的网络日志
阮一峰的网络日志
Webroot Blog
Webroot Blog
Recorded Future
Recorded Future
S
Secure Thoughts
NISL@THU
NISL@THU
Google Online Security Blog
Google Online Security Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Register - Security
The Register - Security
T
The Blog of Author Tim Ferriss
S
SegmentFault 最新的问题
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
大猫的无限游戏
大猫的无限游戏
P
Privacy & Cybersecurity Law Blog

Posts on WKLKEN THINKING

apisix 中的 lrucache apisix 中的服务发现机制 apisix 中的负载均衡 apisix etcd机制 聊聊框架 关于 k8s 的 zero downtime deployment 一些建议 apisix 遇到的一些问题 关于在除夕前一天换了一个洗衣机的故事 Django DRF 性能优化 DRF继承关系图 Better Code: 关于接口的灵活性 新的仓库: wklken/naming 缓存使用的一些经验 Better Code: 抽象: 可扩展性与可维护性的抉择 Better Code: 异常时, 该提示用户哪些信息? Better Code: 更好的异常日志打印 Go: some libs Go: go-redis/cache升级的坑 Go: logrus性能提升 Go: gin validation 远程办公的一点总结 Go: 开发过程中的一些bug 项目管理实践: 风险驱动开发 Go: 一种error wrap调用链处理方式 漫谈技术选型 Go: 基于 apitest 做handler层单元测试 Go: go-sql-driver interpolateparams参数优化 [分享]深度工作 你需要更多的思考时间 Django项目重构小结 工作七年小结: 学习,生活及其他 [分享]bash日常: bash-utils 极客时间推广海报 2017总结: 予时光以意义 k8s APIServer源码: api注册详细细节 k8s APIServer源码: api注册主体流程 k8s APIServer源码: 服务启动 k8s APIServer源码: go-restful框架 重构 - 读书笔记(Python示例) 写给新人的沟通建议 vim 杂谈 - 关于快速编辑 vim 杂谈 - 关于移动 读书笔记-重构: 章11 处理概括关系 读书笔记-重构: 章10 简化函数调用 读书笔记-重构: 章9 简化表达式 读书笔记-重构: 章8 重新组织数据 读书笔记-重构: 章7 在对象之间搬移特性 读书笔记-重构: 章6 重新组织函数 Python 代码规范小结 [分享]关于vim ElasticSearch集群部署文档 Logstash+ElasticSearch处理mysql慢查询日志 [分享]关于代码调试DE那些事 Logstash+ElasticSearch+Kibana- 实现相对通用的数据收集分析 ELK维护的一些点(二) [分享]Python源码剖析-数据结构 一些Centos Python生产环境的部署命令 摘录<<6个月学会任何一种外语>> ELK 维护的一些点 也许是一个新的开始 一些vim的个性化配置 读书笔记-调试九法 这段时间的一些想法 Python 源码阅读 - 垃圾回收机制 我为什么要写博客 APUE笔记-第一章 UNIX基础知识 Python源码阅读-闭包的实现 Python源码阅读-内存管理机制(二) Python源码阅读-内存管理机制(一) Python-基础-数据结构小结 '活动'设计的一些trick 一些简单的Python测试题 我的tmux配置及说明【k-tmux】 Review and Restart 工作四周年小结 vim插件: surround & repeat[成对符号编辑] vim插件: gundo[时光机] vim插件: expand-region[区域选中] vim插件: quickrun[快速执行] vim插件: trailing-whitespace[行尾空格处理] vim插件: closetag[成对标签补全] vim插件: ctrlp[文件搜索] vim插件: airline[状态栏增强] vim插件: theme[主题] vim插件: tagbar[大纲式导航] vim插件: nerdcommenter[快速注释] vim插件: rainbow_parentheses[括号高亮] vim插件: syntastic[语法检查] vim插件: delimitmate[符号自动补全] vim插件: matchit[成对标签跳转] vim插件: easy-align[快速对齐] vim插件: multiple-cursors[多光标操作] vim插件: vim-signature[快速标记跳转] vim插件: easymotion[快速跳转] vim插件: vundle[管理插件] Elasticsearch几个问题的解决 分享一份 Vim 简介PPT k-vim 更新9.0版本 关于知识管理工具的思考 Logstash+ElasticSearch+Kibana处理nginx访问日志
DRF 的一些实践 Part1: Serializer
2022-10-09 · via Posts on WKLKEN THINKING

SLZ 只做输入/输出相关的逻辑, 尽量避免处理具体的业务逻辑

命名建议

  1. 写全 xxSerializers
  2. 也可以用缩写xxSLZ(注意全大写, 不要用xxSlz)

两种风格

1. 全部serializer放在一起

同一个application的全部serializer放在一起

application
    |- serializers.py

如果都放在一起, 建议区分 inputoutput

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

官方文档: 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")

避免:

  1. 暴露不必要的字段出去, (场景: 未来字段变更想去掉, 如果原来没有暴露, 那么直接去, 否则需要找调用方逐一确认)
  2. 不小心暴露不能更新的字段出去, 例如create_time/update_time

单一职责

单一职责原则: 一个slz只给一个view使用, 尽量避免多个view共用一个slz

不一定需要继承: 复用 vs 不复用

极端处理: 每个 slz 都是独立的, 及时存在公共字段, 也不抽父类出来;

带来的后果: 字段的变更/校验逻辑变更等, 需要改动涉及的所有 slz

使用继承: 变更只需要改一个地方, 但是存在风险, 改一个地方可能动到意想不到的其他接口校验/返回

倾向的做法:

  1. 刚开始开发, 完全不考虑继承, 完全独立
  2. 主体逻辑和接口开发完之后, 可以根据需要加入部分继承, 例如公共的create_time/update_time等字段
  3. 如果使用继承, 尽量减少继承的层级(不大于 3 层), 并且需要考虑隔离: 输入/输出的 slz 父类进行隔离, 或者核心模块slz和非核心模块slz隔离, 即使它们有相同的字段;(缩小未来变更的影响范围)