动态数据模型分析与应用

一、背景

笔者所属公司是一个电商公司,全国有很多的门店,所有业务系统都围绕着门店进行,门店原有一个营建系统用于选址开店、门店基础信息配置、门店业务信息配置。在门店营建系统重构之前,门店基础信息主要面临三方面问题:
1.门店基本信息分散在配送、门店、基础数据等多个系统中,每个系统除了存储门店的公用的基础信息(门店编码、门店名称、所属组织等)以外,也存储了自己系统所需要的一些独有信息,比如门店系统存储了门店作业的相关信息。由于门店信息的维护往往面对的用户群体是相同的,但是数据分散在多个系统,从而导致获取一份数据要跨多个系统进行线下的记录与整合,无法高效获取门店基础信息。

2.各个系统维护的门店基础信息权限管理比较粗放, 不能在门店属性级别上做权限管理。

3.对于门店基本信息的获取没有统一的标准接口供需求系统获取,对于获取同一份门店基本信息数据可以在多个系统中获取到,增加决策成本。

在解决上面问题之前,必须向大家解释一下上文提到的基础数据这个系统的作用,这个系统原来的定位就是把原来物流云平台的所有基础数据(大区信息、大仓信息、仓组信息、门店信息、SKU信息)等整合起来供业务系统使用。所以它主要有2个功能,第一、从各个业务方同步基础数据;第二、提供给各个业务方查询的RPC接口。这样看来基础数据已经解决上文问题1的大半和问题3的全部。其实并不是这样,基础数据只存储了门店的基本信息并没有提供编辑入口,同时在门店维度上各个业务系统对于门店信息的维护变化多端,所以以当前物流基础数据的数据结构,没办法支持动态扩展字段。

基于上面的考虑,面对上文中提出的3个问题,我们决定另建一个系统—营建系统。
1.建立一个更顶层的系统把门店信息的维护入口全部统一到这里,同时提供一个标准服务接口供外部系统获取门店基本信息。所以我们提出两步走策略:第一步,统一门店信息维护入口和门店信息查询接口,门店维护的门店基本信息依然同步到物流基础数据。第二步,彻底替换物流基础数据。

动态数据模型分析与应用_第1张图片


2.营建服务要建立一个动态数据模型,模块化管理各个业务方的门店数据,模块中的字段要可以动态配置避免未来业务方对于门店属性的新增编辑让作为基础服务的营建服务频繁修改发版,同时要对门店信息的权限能能够精细化管理精确到字段维度

解决方案中的第1点,是一个系统架构设计的问题,本文就不再赘述,下面我们来重点讲讲第2点如何实现一个动态数据模型。

二、动态数据模型的选择

目前市面上主流的动态数据模型有3种,他们各有优劣,下面我们就详细介绍他们的实现方式和优缺点,笔者在此声明这里的优缺点对比只是针对我当前所做业务的对比,这里提供的是一个技术选型的思路,大家在选择技术实现的时候可根据自己的业务进行。

2.1 JSON存储动态数据

除了一些基础字段外对于动态数据全部存储到mysql的json字段中,动态字段的编辑都通过修改json的属性实现。基本数据结构如下:

动态数据模型分析与应用_第2张图片

优势

1.使用SON存储代码实现简单,对于门店动态属性对象直接转为JSON后直接存储到数据库,开发效率高。
2.对于动态字段的查询修改mysql都有对应的函数支持。

劣势

1.mysql中对于json字段的函数对mysql版本敏感,对于mysql 5.7以下的版本就不要考虑使用这种方式了。
2.没办法进行join查询。
3.查询效率低。
营建服务并没有使用json存储动态字段,其主要原因也不是因为上面的三个劣势,营建服务的要求门店信息划分为两个维度,分别是模块和动态字段。一个门店有多个模块的动态数据,每个动态模块的数据下又有多个动态字段。如果我们要用JSON存储方案就会有两种数据结构,第一种是上图中的动态实体中增加模块属性字段,那么一个门店就会存在多个动态实体,其实上对于模块来说,这种方式反而变成了下文中要提到的行转列。第二种方案是我们把模块信息存储到JSON字段中,这样就会让json中字段是对象的情况,那么mysql的json查询编辑函数就不能够支撑我们的业务需求了。

2.2 预定义冗余列存储动态数据

预定义许多不同类型冗余的列作为动态字段的槽位,动态数据的值都放到对应的槽位中,数据结构如下:

动态数据模型分析与应用_第3张图片

优势

1.统计数据方便,因为相同的业务字段都在同一列查询和统计都非常方便。
2.支持join操作。

劣势

1.实现复杂,需要实现一套复杂的动态sql逻辑。
2.查询效率相对较低,无法直接在字段上添加索引。
3.前期需要预估好各个类型字段的最多数量,达到最多数量后扩展十分麻烦
营建服务并没有使用这种方案,主要出于三方面考虑,第一、当时营建服务的动态字段并不需要join查询,并且门店的统计数据不会涉及到动态字段;第二、当时工期比较赶,无法在短时间内实现这套方案;第三、营建服务未来要承接所有业务方的动态字段,无法预估各种类型字段的最大值。

2.3 行转列存储动态数据

通过建立两张表一张表存储公共数据,另一张表存储动态数据,公共数据表和动态数据表是一对多关系,数据结构如下:

动态数据模型分析与应用_第4张图片

优势

1.开发速度快,因为其基本数据结构依然是mysql的结构性数据研发速度快。
2.可以配置无数个不同属性,以应对应应对快速变化的业务需求。

劣势

1.没办法进行join查询。
2.如果遇到统计数据报表的时候实现会比较麻烦
3.通过动态字段查询比较慢
我们最终选择的方案是行转列,第一、我们的开发工期比较短;第二、业务方太多难以统计各方各个类型字段的最大值;第三、门店信息无join查询和报表导出;第四、当前营业中门店不超过600家,数量不大,所以我们用了分布式缓存的方案来解决查询慢的问题。

三、营建服务动态数据模型实践

基于上文中的思考,我们采用了行转列的技术方案,接下来我们来详细介绍行转列在营建服务中的具体实现细节。

3.1 数据模型

对于门店信息中的公共字段如门店编码、门店名称、门店位置等信息我们依然用传统数据格式存储在一张基础信息表中,对于动态字段如营业执照、房屋合同等信息存储在动态动态属性表中具体数据结构如下:

动态数据模型分析与应用_第5张图片

3.2 布局

布局包含两个部分模块布局和字段布局,目前营建的模块和字段的布局主要是控制各个模块和字段的展示顺序,当然我们当前的数据结构是支持字段的更精准的控制的比如每行显示几个字段,字段显示宽度和高度等都可以。由于当前模块的设计是所有门店共用同一个模块,所以模块的布局我们只是在模版表中添加了一个order_by字段来控制其显示顺序。

动态数据模型分析与应用_第6张图片


模版下的字段我们则是通过在模块表中添加一个JSON字段来控制模版字段的布局。由于当前业务中只需要控制模版字段的显示顺序,但是为了方便未来扩展我们并没有像模块布局一样在模块下字段中加一个字段来实现排序,我们在模版表中添加了一个JSON字段用来控制其下字段的布局,这样能够防止调整一个字段的顺序而引起模块下所有字段排序字段的更新同时也能够方便未来扩展控制更为复杂的字段布局。上图中门店base表中的字段也会在模块中配置,所以基础字段的布局也可以被控制。

3.3 字段

在字段的实现中如果从技术角度分类可以分为三大类:内部引用字段、引用字段和动态字段。从业务角度分为了:文本、链接、单选多选、日期、数字、条件字段以及组合字段等。

动态数据模型分析与应用_第7张图片


接下来从技术角度分类讲一下各种类型的实现方式:

3.3.1 内部引用字段

内部引用字段可以简单的理解为存储在门店base表中的公共字段。之所以把公共公共字段也要在模版中定义的原因是为了控制公共字段的布局以及字段的权限,同时把所有门店信息的业务字段校验统一逻辑。这种字段在模块字段表中有一个in_reference字段,这个字段标示着门店base表对应的类的一个属性。比如门店编码在门店base表对应的属性是warehouseCode那么in_reference字段的值就是warehouseCode。门店编码这个属性的值不会存储在微仓属性表中,而是存储在门店base表中。在编辑对应字段是后端统一接口都是接收的动态字段。后端服务查询到对应字段如果in_reference的值不为空则会通过反射把对应的值存储到门店base表中。

3.3.2 动态字段

动态字段的定义非常简单就是这个字段的value值存储在门店属性中,他的实现逻辑非常简单不再详细介绍。

3.3.3 引用字段

所谓的引用字段就是不可以通过web页面编辑,而是他的值通过它配置的引用字段的值通过计算得到的值。比如说我们有一个门店等级字段。他是通过门店面积计算门店等级的,大于150平为S级,大于120平小于150平是A级……,也就是说在编辑门店面积的时候这个字段也要跟着变化。在模块字段表中我们配置了一个reference_field_id字段这个字段记录着它引用的字段,同时会在save_script中记录着引用字段的计算逻辑脚本。当一个字段被编辑时,会查询这个字段是否被其他字段引用如果被引用那么查看,其对应的计算脚本根据脚本编辑这个引用字段的值。

可能你会问为什么引用字段一定要存储下来?从上面的描述似乎引用字段只是展示,那么在展示的时候计算一下不就行了?其实我们主要是基于引用字段也会被作为查询条件的考虑才把引用字段也存储起来。上文中提到了存储脚本计算,我们主要是引入了一个动态脚本语言来实现的,关于这个细节的实现不再赘述。

其实引用字段和内部引用字段以及动态字段字段有重合的部分,引用字段也可以属于内部引用字段也可以属于动态字段,但是内部引用字段和动态字段是严格区分不重合的

从业务上我们又把字段划分为以下类型:

3.3.4 文本字段

文本字段又细分为单行文本与多行文本,在模版字段中有两个字段是min和max,分别用于控制文本的最短和最长。

3.3.5 单选多选

单选和多选字段通过在模版字段配置中添加一个配置JSON字段init_config用于控制,字段所有的选项。JSON字段格式如下:

[
    {
        "label":"个人",
        "value":"1"
    },
    {
        "label":"公司",
        "value":"2"
    }
]

3.3.6 链接字段

链接字段又分为图片、视频、链接三种,在数据中存储的值都是用逗号分隔的文件链接。此时模版字段中的min和max控制可以上传的图片、视频以及链接的最大个数。

3.3.7 时间字段

时间字段分为日期、月份、时间三种,都以时间格式化后的字符串存储在数据库中。

3.3.8 数字字段

数字字段又分为正数、小数两种,直接把数字转化为字符串存储在数据库中。

3.3.9 组合字段

组合字段是由多个字字段组成,假设说一个班级是一个实体,那么班级里的学生就是一个组合字段,组合字段的子字段是学生姓名、年龄等,所以组合字段在页面的展示形式是一个表格。组合字段字段的定义同样存储在模版字段表中,而组合字段本身的定义存储在模版字段的init_config列中,其用json数组存储了它所有的子字段id。格式如下:

["FI-00020143","FI-00020142"]

组合字段的子字段不再分开存储,而是全部存储在组合字段的value中其格式如下:

[{
    "FI-00020143":1,
    "FI-00020142":"文本字段值"
}]

数组的形式存储着子字段的值,数组中每一项的key对应字段id,value对应字段的值。

3.3.10 条件字段

条件字段是当你选择某个条件时,就要填写在这个条件下的子字段的值。他的子字段也是存储在模版字段表中,定义存储在init_config中格式如下:

[
    {
        "fieldIds":[
            "FI-00020128"
        ],
        "labelName":"是",
        "value":"1"
    },
    {
        "fieldIds":[
            "FI-00020127"
        ],
        "labelName":"否",
        "value":"2"
    }
]

其中fieldIds就是对应选择条件下的子字段ID。
其子字段的值不再分开处理,全部存储在条件字段中,存储格式如下:

{
    "conditionValue":1,
    "subValues":{
    "FI-00020128":"子字段值"
    }
}

其中conditionValue对应的值就是条件值,subValues中每一个key对应子字段id,value对应子字段的值。

四、代码实现

上文详细介绍了各个字段的存储结构,接下来我们讲讲代码实现,从上文不难看出不同的业务字段要有不同的实现方式,那么我们选择设计模式中的策略模式来实现不同的字段逻辑。其实对于所有的字段无非就是三种操作:
1.字段新增/编辑的时候的校验。
2.字段的数据值转换成对应字段格式的存储值。
3.字段的存储值转换成展示值。

所以我们抽象出来的公共类如下:

public abstract class ModuleFieldStrategy {
    /**
     * 导入对象转化为存储值
     *
     * @param moduleFieldDOMap
     * @param fieldDO
     * @param fieldColumn
     * @param subColumns
     * @param parentRequired
     * @return
     */
    public abstract WarehouseAttributeDTO importConvert2PO(Map moduleFieldDOMap, ModuleFieldDO fieldDO, ColumnDTO fieldColumn, List subColumns, Boolean parentRequired);

    /**
     * 参数校验
     *
     * @param fieldValue
     * @param parentRequired
     */
    public abstract void fieldValueValidate(Map moduleFieldDOMap, ModuleFieldDO fieldDO, ModuleFieldValueDTO fieldValue, Boolean parentRequired);


    /**
     * 转换为展示对象
     *
     * @param fieldValue
     * @return
     */
    public abstract ModuleFieldValueDTO parse2VoValue(WarehouseAttributeDTO fieldValue, ModuleFieldDO fieldDO, Map allFieldMap);

    }

各种类型的字段通过实现上面的类,来实现自己的业务逻辑,对于组合字段和条件字段比较特殊,他会循环调用自己的子字段的策略来实现自己的业务逻辑,最后在业务操作时根据字段类型获取相应策略即可。

五、权限控制

目前的权限控制主要是对用户角色控制模块、字段的编辑和展示;在模块模版表以及字段表中都有一个字段is_show,具体的权限在权限系统配置对应模块的id,如果对应角色没有模块权限并且is_show 字段是true那么该字段就不会在前端显示。这一块的控制主要放在了权限系统和前端,并且控制实现逻辑也非常简单就不再细说。

六、查询优化

在本文最初已经提到过,营建服务需要提供统一的查询入口,由于门店字段非常多,不可能每接入一方我们就要开发一个接口,于是我们参考了大数据团队的技术方案,让调用放传入需要查询的字段id以及查询条件,营建服务返回对应字段id的值,传入参数:

public class WarehouseConditionDTO extends ClientDTO implements Serializable {

    private static final long serialVersionUID = -2304572913021892020L;
    /**
     * 返回结果字段列表
     */
    private List resultList;
    /**
     * 条件列表
     */
    private List whereList;
    /**
     * 排序列表
     */
    private List orderList;
    /**
     * 页码,第一页从1开始
     */
    private Integer pageNum;
    /**
     * 分页大小,默认10,最大100
     */
    private Integer pageSize;
}

参数示例:

{
    "appCode": "sms-site", 
    "appSign": "nbynkFKYSPrtMrOs", 
    "resultList": [
        "FI-10003", 
        "FI-10004"
    ], 
    "whereList": [
        {
            "fieldId": "FI-10035", 
            "symbol": "=", 
            "values": [
                "S"
            ]
        }
    ], 
    "orderList": [
        {
            "fieldId": "FI-10020", 
            "sortType": "asc"
        }, 
        {
            "fieldId": "FI-10012", 
            "sortType": "asc"
        }
    ], 
    "pageNum": 1, 
    "pageSize": 2
}

七、落地效果及复盘

7.1 落地效果展示

模块配置效果展示如下:

动态数据模型分析与应用_第8张图片


模块配置效果展示:

门店信息应用动态数据后展示效果如下:

7.2 技术复盘

在需求层面,当前营建服务已经完成了所有门店的灰度,收拢了门店信息的出入接口。将门店信息分为了7大模块,7大模块分别授权,不同的运营人员维护不同的模块,各模块可以自由添加自己想要的字段,在100%满足了业务诉求的同时,做到了各个业务模块精细化管理,降低了各个业务方维护同一份数据相互相应产生错误的概率。

在技术层面,由于动态数据模型的引入增加了代码复杂度,同时为了满足业务查询需求,营建服务加入了分布式缓存,这些在实现本次需求中确实增加了大概3pd左右的开发量,但是由于灵活的表单配置方式,在上线后关于各个业务方对于门店信息新增属性,修改属性的开发量直接降为了0,大大降低了未来的研发成本。

八、未来展望

虽然当前数据模型已经完全能够应对当前的业务需求但是还有一些细节需要打磨,比如对于单选多选字段可能选项不是配置出来的而是从第三方系统获取的,因此这里需要实现一个灵活接入第三方系统动态获取枚举的接口。同时对于门店数据多维度的可视化展示也需要完善,能够通过可视化的页面让运营同学更直观的看到门店的数据。

关注公众号,一起学习,共同成长

动态数据模型分析与应用_第9张图片

 

 

你可能感兴趣的:(架构)