DRF 的一些实践 Part1: Serializer

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隔离, 即使它们有相同的字段;(缩小未来变更的影响范围)

pythondrf

1984 Words

2022-10-08 17:00 +0000