Django Rest Framework 可自定义读写 Serializer Field实现(附代码)

Django Rest Framework 可自定义读写 Serializer Field实现(附代码)

背景

对于Django项目开发来说,DRF(Django Rest Framework)几乎已经成为了最热门的接口框架。通过Model+Serializer+ViewSet的方式,我们即可快速完成对某一种Model数据模型的CURD操作接口,可以说是非常方便高效了。

本文基于一个现实工作场景中的需求,实现了一个可自定义读写的Serializer Field,方便开发者在Serializer中自定义接口数据和模型数据的转换逻辑。同时,本文会详细介绍这个自定义实现的逻辑,同时会上传对应代码,方便有类似需求的读者不需要去重复造轮子。

Serializer 介绍

对于了解过Django开发但是没接触过DRF的同学,Model是Django自带的,ViewSet对应于Django的视图View,唯一不太熟悉的可能就是Serializer了。Serializer直接翻译是序列化器,其实就是完成接口数据格式到Model对象数据格式的一个转换过程。在没有Serializer之前,这些转换工作都需要我们自己在接口里面进行实现;在用了DRF的ModelSerializer之后,转换过程我们就不再需要过多关心了,框架已经帮我们做了。

下面先用最简单的代码,来演示默认情况下ModelSerializer的转换结果:

# models.py
class ReadWriteFieldDemoModel(models.Model):
    str_field = models.TextField(help_text="列表拼接成字符串字段")
# serializer.py
class NaiveSerializer(serializers.ModelSerializer):
    class Meta:
        model = ReadWriteFieldDemoModel
        fields = "__all__"
# viewsets.py
class ReadWriteFieldViewSet(ModelViewSet):
    serializer_class = NaiveSerializer
    queryset = ReadWriteFieldDemoModel.objects.all()

可以看到,数据模型对象只有一个字段str_field,是一个TextField,即文本字符串类型。所以,ModelSerializer就会帮我们自动把这个字段和str进行转换,从接口请求拿到的数据和数据库中的数据,我们可以看到获得的都是"test_string"这么一个字符串,这说明Serializer能够正常地将数据模型对象中的str_field字段类型自动识别为str并进行正确地读取和转换,如下图(图一为接口返回数据,图二为数据库数据):

需求

对于上面的简单情况,ModelSerializer看上去已经能够满足我们序列化的基本需求了。但是,在实际项目中,我们经常会遇到这样的需求: 我们希望在接口层使用列表类型的数据,但是在数据模型层将这些数据保存成对应列表元素的字符串拼接结果

实践

要实现上述需求,其实需要再详细拆分一下:

  1. 只希望在读取的时候返回列表格式的数据,数据写入的时候是由后台直接写入数据库或者仍然提交字符串数据。
  2. 希望在读取和写入两种情况下接口层都能使用列表格式数据,接口层不需要感知后台数据层是用字符串保存数据的。

1. 只读情况

如果只是想在Get请求上实现从将str_field转换成列表类型并读取展示,其实DRF已经给我们提供了一种实现方法,即使用SerializerMethodField。

# serializers.py
from rest_framework.fields import SerializerMethodFiel
class ReadMethodSerializer(serializers.ModelSerializer):
    list_field = SerializerMethodField(help_text="Test SerializerMethodField")
    class Meta:
        model = ReadWriteFieldDemoModel
        fields = ("list_field",)
    def get_list_field(self, obj) -> List[str]:
        return [content for content in obj.str_field.split(",")]

可以看到,这里在Serializer中定义了一个list_field,然后再定义一个get_list_field的函数,将从模型对象中str_field字段的值转换成列表格式,就完成了我们只读的需求。效果如下:

可以看到,这里Swagger生成的文档很贴心地告诉我们这个list_field字段是只读的,从请求结果上看,确实转换成了列表格式。所以,如果是只读需求,直接使用DRF提供的SerializerMethodField就可以实现。

2. 读写情况

那么,如果希望读写都能使用列表格式的数据,需要如何处理呢?

这里可以参考SerializerMethodField的具体实现,其实从源码上看还是比较简单的,这里做一些简单的截取和注释:

# rest_framework/fields.py
class SerializerMethodField(Field):
    def __init__(self, method_name=None, **kwargs):
        self.method_name = method_name
        kwargs['source'] = '*'
        kwargs['read_only'] = True  # 设置只读
        super().__init__(**kwargs)
    def bind(self, field_name, parent):
       # 这里获取开发者用于读取时数据转换的方法名method_name,默认为get_{field_name}
    def to_representation(self, value):
       # 利用读取过程的hook,调用get_{field_name}方法进行数据转换
        method = getattr(self.parent, self.method_name)
        return method(value)

这里的源码其实不难理解,整个过程其实就是在bind的是否找到读取时候转换数据所需要调用的方法,然后再在DRF中统一获取数据表示的hook to_representation函数中进行调用转换方法。它相当于是利用了从模型对象数据到接口返回数据的转换过程中的一个hook,在hook中调用了开发者自定义的数据转换函数。

看到这里,其实要实现一个可读写的SerializerMethodField也不难,只需要找到DRF中将接口数据写入到模型对象过程的hook,然后再照猫画虎地在写入过程中多绑定和调用一个开发者自定义的写入转换函数即可。直接看代码:

class ReadWriteSerializerMethodField(SerializerMethodField):
    支持可读写的SerializerMethodField
    可实现Model字段和Serializer字段更加灵活地解绑
    通过实现get_xxx_field方法,实现从Model的某个字段读值映射到Serializer对应字段
    通过实现set_xxx_field方法,实现从Serializer字段回填值到Model对应字段
    def __init__(self, method_name=None, write_method_name=None, **kwargs):
        self.method_name = method_name
        self.write_method_name = write_method_name
        kwargs["source"] = "*"
        super(SerializerMethodField, self).__init__(**kwargs)
    def bind(self, field_name, parent):
      # 绑定读函数 get_{field_name} 和写函数 set_{field_name}
        default_method_name = f"get_{field_name}"
        default_write_method_name = f"set_{field_name}"
        if self.method_name is None:
            self.method_name = default_method_name
        if self.write_method_name is None:
            self.write_method_name = default_write_method_name
        super(SerializerMethodField, self).bind(field_name, parent)
    def to_representation(self, value):
       # 读取过程hook
        method = getattr(self.parent, self.method_name)
        return method(value)
    def to_internal_value(self, data):
       # 写入过程hook
        method = getattr(self.parent, self.write_method_name)
        return method(data)

自定义了ReadWriteSerializerMethodField,我们只需要在我们的Serializer用上即可:

class ReadWriteFieldSerializer(serializers.ModelSerializer):
    list_field = ReadWriteSerializerMethodField(help_text="Test")
    class Meta:
        model = ReadWriteFieldDemoModel
        fields = ["list_field"]