自定义扩展字段的实现(基于Django框架)

   最近项目中需要做一个统一身份与认证平台,其中有用户可以自定义扩展字段的需求,目前在设计阶段,记录一下当前的实现方式。

一.整体实现:

   由于使用Django orm的原因,比较难对表的结构进行改变,所以需要方便的进行动态扩展。主要通过数据字典的方式记录自定义扩展的字段,然后将需要扩展数据存储到json字段或者扩展数据表中。

  数据字典的设计都比较规范,主要是记录对象ID,字段名称,数据类型,是否唯一,是否必填,是否可修改,是否启用。根据业务需要,也可以设置字段精度或者是否索引和是否查询。

二.存储方式参考

(1) 插槽存储

    之前参考了一些云厂商产品在自定义字段中的实现方式,其中做的自由度最高的是salsforce的产品。通过插槽的方式实现自定义数据的存储。salesforce的多租户架构完全实现了用户自定义对象,自定义对象属性,自定义对象之间的关系。通过以下几张表完成了数据定义和数据存储。

    MT_Objects:存储对象的定义,通过ORGID字段实现多租户隔离。

    MT_Fields:存储自定义字段,包括唯一标识符,字段对象,字段名称,数据类型,是否索引,字段位置。

    MT_Data:存储数据。value0-value500个fex列(插槽),存储分别映射到MT_Objects和MT_Fields中声明的表和字段的应用程序数据。每个插槽都存储的是字符串数据,在应用程序读取数据的时候转换为对应的数据类型。

     自定义扩展字段的实现(基于Django框架)_第1张图片

   优点:映射关系简单,索引方便

   缺点:预留插槽少了不够用,多了又浪费

(2)version+ext

即在表后面增加version,ext字段,version用来描述对应的版本,ext存放map的json数据,如{key1:value1,key2:value2……}

uid ... version ext
1xxxxx ... 0 {"name": "Bob"}
2xxxxx ... 0 {"name": "Joly"}
2xxxxx ... 1 {"name": "Joly", "gender": "F"}

优点:可动态扩展字段,不造成空间浪费,查询速度较快,可支持多版本数据
缺点:ext不支持索引,key名称大量冗余,所以key命名尽量短,可使用编码

(3)key+value

    通过一张key_value表实现扩展字段数据的存储。

uid key value
1xxxxx name Bob
2xxxxx name Joly
2xxxxx gender F

    优点:每个字段都方便进行查询,可以自由的扩展字段

    缺点:一条数据会产生多行记录,Key值冗余过多。

 

三.项目具体实现

      根据业务的特性,我们业务场景下基础数据量较小,并且根据扩展字段进行查询的需求极小,基本只做存储和展现,所以选择了方案二(version+ext)。   

       由于项目使用的是Django框架,所以分为模型,序列化,API过滤器三部分分别实现。

(1) 模型设计

     首先需要一个扩展字段表记录用户自定义的扩展字段,其中字段包括扩展字段的名称,key值,字段数据类型,扩展的模型,以及是否查询,是否唯一等一些业务设置。

      需要注意的是(company_id,key,model)需要是关联唯一的。这样保证了扩展字段的系统唯一。

      业务模型需要增加一个json字段用来记录扩展字段的值。

class FieldType(object):
    """字段类型枚举"""
    STRING = 1
    DATE = 2
    BOOL = 3
    INT = 4
    FLOAT = 5

    MESSAGE = {
        STRING: _("字符串"),
        DATE: _("日期"),
        BOOL: _("布尔值"),
        INT: _("整数"),
        FLOAT: _("浮点值"),
    }


class ExpandField(object):
    """扩展字段表"""
    name = models.CharField(max_length=32, help_text=_("字段名称"))
    key = models.CharField(db_index=True, max_length=32, help_text=_("字段编码"))
    description = models.TextField(blank=True, null=True, help_text=_("描述"))
    field_type = models.SmallIntegerField(choices=FieldType.choices(), help_text=_("字段类型"))
    model = models.IntegerField(help_text=_("所属分类"))
    is_built_in = models.BooleanField(default=False, help_text=_("是否内置"))
    is_required = models.BooleanField(default=False, help_text=_("是否必填"))
    is_modified = models.BooleanField(default=True, help_text=_("是否可修改"))
    is_unique = models.BooleanField(default=False, help_text=_("是否唯一"))
    is_query = models.BooleanField(default=False, help_text=_("是否可查询"))
    enabled = models.BooleanField(default=True, help_text=_("是否启用"))
    company_id = models.CharField(max_length=32, help_text=_("公司ID"))

(2) 序列化

     因为和前端约定好扩展字段不和系统字段放在同一级,所以序列化实现和常规的序列化差别不大,只是需要重载一下init方法,转换一下数值的类型为字符串。不过我在这边也实现了把扩展字段外放到第一层级。

class BaseExpandSerializer(serializers.ModelSerializer):
    def __init__(self, instance=None, data=empty, **kwargs):
        data = self._extract_expand_data(data)
        super(BaseExpandSerializer, self).__init__(instance=instance, data=data, **kwargs)

    def _extract_expand_data(self, data):
        if data == empty:
            return data
        current_model = getattr(self.Meta, "model")
        model_category = {} # 模型类型映射表
        if current_model in model_category:
            expand = data.pop("expand", {})
            for key in list(expand.keys()):
                expand[key] = str(expand[key])
            data["expand"] = expand
        return data

    def update(self, instance, validated_data):
        new_expand = deepcopy(instance.expand)
        new_expand.update(validated_data.get("expand", {}))
        validated_data["expand"] = new_expand
        return super().update(instance, validated_data)

    def get_expand(self, obj):
        current_model = getattr(self.Meta, "model")
        model_category = {} # 模型类型映射表
        if current_model in model_category:
            expand = obj.expand or {}
            fields = get_model_expand_field(school_id=obj.company_id, model=model_category[current_model])
            for field in fields:
                key, field_type = field["key"], field["type"]
                value = expand.get(key, None) # 未设置的字段置None
                if value: # 转换数据类型
                    if field_type == FieldType.STRING:
                        value = value
                    elif field_type == FieldType.INT:
                        value = int(value)
                    elif field_type == FieldType.FLOAT:
                        value = float(value)
                    elif field_type == FieldType.BOOL:
                        value = bool(value)
                    elif field_type == FieldType.DATE:
                        value = value
                expand[key] = value
            return expand
        return
    
    @property
    def data(self):
        data = super().data
        expand_data = self.get_expand(self.instance)
        data.update(expand_data)
        return data

(3) API过滤器

  这里也是继承了django_filters,这里的查询因为业务比较简单,所以没有支持根据字段类型进行不同功能的查询,比如日期进行范围查询,数字的大小查询等。我这边只实现了字符串精确查询。大家可以后面自行补充。

  未解决的问题:在FilterSet的这个环节,没有想到比较好的方法把视图层的company_id穿透过来,所以使用了hasattr这个笨办法来判别是否是扩展字段,而且这样会和分页等参数冲突。如果大家有比较好的方法,欢迎评论和指导。

class BaseExpandFilter(django_filters.FilterSet):
    system_key = ["page", "page_size"]

    def filter_queryset(self, queryset):
        queryset = super().filter_queryset(queryset)
        return queryset.filter(**self.expand_data)

    def extract_expand_data(self, data):
        model_str_list, expand_data = [], {}
        current_model = self._meta.model
        clare_fields = self._meta.fields
        keys = list(data.keys())
        for key in keys:
            if not hasattr(current_model, key) and key not in self.system_key and key not in clare_fields:
                expand_key = "expand__{}".format(key)
                expand_data[expand_key] = data.get(key)
            else:
                query_str = "{}={}".format(key, data.get(key))
                model_str_list.append(query_str)
        model_str = "&".join(model_str_list)
        model_data = QueryDict(model_str)
        setattr(self, "expand_data", expand_data)
        return model_data

    def __init__(self, *args, **kwargs):
        model_data = self.extract_expand_data(kwargs["data"])
        kwargs["data"] = model_data
        super(BaseExpandFilter, self).__init__(*args, **kwargs)

   关于扩展字段的方案归纳和基于django框架的实现到这里就差不多了,虽然还有些未解决的问题,后期如果找到比较好的方法再更新上来。也许有很多实现不太好的地方,欢迎大家讨论和指导,共同进步。

你可能感兴趣的:(python后端)