袋鼠云数据资产平台:数据模型标准化建表重构升级

数据模型是什么?简单来说,数据模型是用来组织和管理数据的一种方式。它为构建高效且可靠的信息系统提供了基础,不仅决定了如何存储和管理数据,还直接影响系统的性能和可扩展性。

想要建立一个良好的数据模型,设计时需要优先考虑数据的关系和规范化,避免出现数据冗余和不一致的问题,减少数据维护的难度。

正是基于这样的需求,袋鼠云数据资产平台中的数据模型提供了一种标准化建表的能力,可以对表名、字段名等信息进行标准化约束,并且支持批量解析模式(根据中文名批量解析字段)与建表语句模式进行快速的模型搭建。

下面是其界面功能展示,针对不同的数据源类型,可配置的字段属性与交互方式也不尽相同。 file file

数据资产数据模型标准化建表一开始是只针对建Hive表的,但随着后续迭代改为数仓表后,会不断支持离线开发中的更多数据源类型。但由于历史代码书写原因,目前存在以下问题:

  • 数据流管理混乱,未遵守单向数据流原则,导致常常出现页面展示与实际提交数据不一致的情况;
  • 表单完全受控,性能问题显著,当字段数达到30+时,表单总数来到300+,输入型表单明显掉帧;
  • 迭代中加入的字段间交互越来越多,没法解决表单间交互依赖问题;
  • 代码维护成本过高,工时评估普遍偏长;
  • 不同数据源类型交互、字段差异性管理,需要维护的枚举会越来越多,需要统一管理。

为了更好地满足客户需求,提升用户体验,袋鼠云数栈UED团队根据政策导向、市场环境、行业趋势和用户反馈,对数据资产平台的数据模型标准化建表资产源代码进行了重构升级。

架构设计

标准化建表主要分为两个部分,表基本信息配置与字段结构配置。针对以上痛点问题,本次数据模型标准化建表的主要重构点为字段结构配置的以下四个方面。

  1. 数据流 数据流管理是 React 前端开发流程中最重要的一环之一,良好的数据流管理,能使组件之间进行高效的状态共享,状态变化溯源与提高代码可维护性。 数据模型数据流目前的情况: file

可以看出原始代码中的数据流管理是非常混乱的,组件间状态随意共享,ref 跨多个层级的访问,其中为了能在进行步骤条切换时仍能保留状态信息,还在最外层组件保留了每个步骤组件的状态信息。 我们重新设计组件的嵌套结构,与状态存储的方式,使其符合单一数据源与单向数据流基本原则。 重构后数据流图如下,引入了Context层来精简数据流。 file

  1. Context层

最外层组件维护 tableData数据源来保存表单数据,通过 TableModel.ProviderContext来向所有子组件共享该数据源,避免 Props 传递过深。 Form.Provider为 antd提供的表单间交互方式,目前 Step1 与 Step2 之间的交互并不多,主要是切换数据源类型时将 Step2 的数据清空,我们所有表单间交互都在外层组件中 onFormChange中去定义。 KeepAlive.Provider是用来缓存每个步骤组件实例的,我们无需再在步骤切换时缓存每个组件内的状态信息至父组件。KeepAlive功能由于 React 不支持,我们自行通过 Suspense实现。

  1. Form层

原来的 Form组件的作用仅仅是用来做表单校验,实际又用了 state 去做数据收集,带来了严重的数据不一致问题,所以重构后完全采用 Form进行数据收集。 实现FormList FormList用于实现第二步的可编辑表格功能,其使用方式与 antd 的 Form.List基本相同,但我们采用自己实现的 FormList组件。 为何不直接使用 antd 的 Form.List ? Form.List会给其内部所有的表单项拼接上 List 字段名,如 [”columnInfoList”, 0, “columnType”] ,但诸如分区字段类型,分区详细信息是与 columnInfoList字段同级的,没法在 Form.List中定义绑定在全局的字段。 FormList中自行实现增、删、移动等基本功能,比较 Array 来判断是否要重新渲染,每个 item 项可自行决定是否拼接 List 前缀 […field.fieldName, “columnType”], 并通过 Context的方式向子组件传递 List 字段名 使用方式: {(fields, operation) => ( <>

        <Button
            onClick={() =>
                operation.add({
                    isPartition: true,
                    isAdd: true,
                })
            }
        >
            新增
        </Button>
    </>
)}
Form视图依赖处理 由于 Form 的局部渲染机制,只对改变的字段进行渲染,所以为了简化表单项依赖代码,封装两个组件方便定义重新渲染时机。 数据模型中一个表单项是否需要重新渲染有三种情况: list 中当前行的依赖的字段值改变了才 rerender, 比如当前行勾选主键则禁用当前行的非null列。 list 所有行中某个依赖的字段值改变了都 rerender, 比如勾选了其中一个分区字段,其他所有行的分区字段列都禁用掉,都需要进行一次 rerender。

Form 层级中的表单项改变了需要进行 rerender。 针对第1、2种情况,封装 FormListDependency, 传入 index表示依赖的具体行,deps表示依赖的字段名。 deps如果是 ["fieldA"]的形式则是上述第一种情况 , 如果是 ["$list", "fieldA"]则是第二种情况。 一个例子:只允许一列为主键,勾选后其他所有主键列禁用, 且与当前列的分区字段互斥。 主键列的定义实现代码如下: <FormListDependency index={index} deps={['isPartition', ['$list', 'pk']]}> {(record, columnInfoList) => { const pkIndex = columnInfoList.findIndex((item) => item.pk); const disabled = pkIndex !== -1 && pkIndex !== index || record.isPartition; return ( <FormItem name={[...field.fieldName, 'pk']} initialValue={false} valuePropName="checked" > ); }}

针对第3种情况,封装 FormDependency 组件,使用方式同 FormListDependency,不需要额外传入下标参数。 注意:对于动态列,即可能展示或者隐藏的列,都需要在对应FormItem上加上preserve=false ,如选择了 VARCHAR 字段类型,会自动设置 dataLength ,但切换成其他类型,就应该清除 dataLength ,这也是完全采用 Form 管控的一个很大的好处,无需再关心动态列的脏数据问题。 至此,视图层的依赖我们已经解决,下面我们需要处理逻辑数据层的交互依赖。

  1. 交互依赖联动

选中某个字段后,自动设置关联的另外一个字段的值,这种交互在数据模型中会很多。依赖具有传递性,如依赖为A ⇒ B ⇒ C,如果使用以前的方法,我们通常会在A表单项 onChange时手动改变 B 和 C ,但其实 C 是与 B 才是直接关联的,我们可能会考虑不到还要同步设置 C 的值。 参考 Formily 的设计, 依赖可分为主动模式和被动模式 。 主动模式就是类似于 onChange时主动去处理其依赖项的数据变动,优点:更符合开发习惯且更加灵活。 被动模式就是监听某个字段,字段变化时处理自身数据变动,优点:字段依赖显式声明,更清楚的表示依赖关系。 由于我们是列表型表单,如果是使用被动模式,当出现 A 列变更后,所有行的 B 列同步修改这种1对N依赖情况,需要对所有行的 B 列添加 A 依赖,那么当 A 变化时,所有行的 B 列添加的监听回调会执行N次,如果采用主动模式,则只需要在一次监听回调中修改所有行即可。因此,我们采用主动模式。 实现 useFormListReactions 封装一个hooks useFormListReactions 来做依赖管理。 我们需要预先定义字段与该字段改变时的 effect, 当触发字段改变时,会传递当前字段改变后的值,字段在列表中的下标,该行的完整值,表单的完整值,set 方法等。 在进行依赖注册时,如果字段是全局字段,非 List 中的某一项,则可以添加 isExternalField=true,在 effect中只需要设置直接依赖项的值即可。 const { notify } = useFormListReactions( [ { fieldName: 'dataType', effect(field, { index, item, allFieldsValue }, { setColumnFieldsValue }) { field === 'VARCHAR' && setColumnFieldsValue(index, { isPartition: 1 }); }, }, { fieldName: 'partitionType', isExternalField: true, effect(_field, _values, { setFieldValue }) { setFieldValue('xx', xx) }, }, ], { form, formListName: 'columnInfoList' } ); 正常情况下,在 Form 的onValuesChange中能监听到用户手动交互时改变的字段,像 setFieldValue 等手动修改情况无法监听到,所以需要对 Form 原生的 set 方法进行一层包装,统一使用包装后的 set 方法我们才能完全监听表单变化。 我们侵入 onValuesChange 中,并使用 hooks 返回的 notify 方法进行消息通知。 <Form form={form} name="tableStructInfo" onValuesChange={(changedValue) => { // columnInfoList.[3].dataType => ['columnInfoList', 4, 'xxx'] const namePath = getObjectNamePath(changedValue); // 具体的值 const fieldValue = getChangedValueByNamePath(changedValue, namePath); notify(namePath, fieldValue); }}

hooks 中提供三种 set 方法, setFieldValue与 setFieldsValue与 Form 的完全一致,setColumnFieldsValue用于修改 List 中某行的值。 const setFieldValue = (name: NamePath, value: any) => { const oldValue = form.getFieldValue(name);

form.setFieldValue(name, value);
if (checkValueChanged(oldValue, value)) notify(name, value);

};

const setFieldsValue = (values) => { const namePaths = Object.keys(values); const oldValues = form.getFieldsValue(namePaths);

form.setFieldsValue(values);
namePaths.forEach((namePath) => {
    if (checkValueChanged(oldValues[namePath], values[namePath])) {
        notify(namePath, values[namePath]);
    }
});

};

const setColumnFieldsValue = (columnIndex, fieldsValue) => { const updateFieldNames = Object.keys(fieldsValue); const notifyList = []; // 批处理完所有setFieldValue后再进行notify, 否则在effect中拿到的其他字段值不是最新的 updateFieldNames.forEach((fieldName) => { const name = [formListName, columnIndex, fieldName]; const oldValue = form.getFieldValue(name);
const newValue = fieldsValue[fieldName]; form.setFieldValue(name, newValue); if (checkValueChanged(oldValue, newValue)) { notifyList.push(notify.bind(this, name, newValue)) } }); while(notifyList.length) { const cb = notifyList.pop(); cb(); } }; 所有交互产生的副作用我们都通过 useFormListReactions 进行管理,它能够自行处理依赖传递行为,且声明式管理使字段变更后可以很方便的进行溯源。 图片 性能优化

为了提升客户使用过程中的交互体验,优化产品性能表现,本次重构后我们完全采用 Form 做数据收集,能够做到足够精细化的局部渲染,相比重构前几十个字段输入框输入就卡顿严重,重构后在几百行字段数量级下表单输入不会造成非常明显的掉帧现象。

  1. 重写 inititalValue,减少初次渲染时 shouldUpdate 调用次数

antd 的按需渲染依赖于 shouldUpdate,假如我们有300行数据,每行有三列字段设置了依赖,那每次表单变化都会触发300 * 3 = 900次 shouldUpdate的执行,这点是没法避免的。 数据模型的动态表单会在该表单上设置 inititalValue, 当表单字段设置 inititalValue时会单独触发一次 storeChange, 导致执行900次 shouldUpdate。编辑时首次渲染,900个字段设置了 initialValue,那么shouldUpdate触发次数将达到 900 * 900 = 810000次,初次渲染会长时间的无反应。 file file 且antd实现的Form.List也是不允许我们设置动态字段的initialValue的。 注意:Form.List 下的字段不应该配置initialValue ,你始终应该通过 Form.List 的initialValue或者 Form 的initialValue来配置。 在数据模型场景下 initialValue 尤为重要,所以我们需要自行实现 initialValue 逻辑,对 Form.Item 进行二次封装,代理掉默认的 initialValue 行为。 const FormItem = ({initialValue, ...restProps}) => { const form = Form.useFormInstance();

useEffect(() => {
    if (initialValue !== undefined) {
        if (
            restProps.name &&
            form.getFieldValue(restProps.name) === undefined
        ) {
            form.setFieldValue(restProps.name, initialValue);
        }
    }
}, []);

return <Form.Item {...restProps}/>

} 替换所有 FormItem后,编辑时首次渲染 shouldUpdate调用次数降为1。

  1. 使用KeepAlive,减少切换步骤时的卡顿

以200行,每列11个字段的 StarRocks 为例: 未重构前切换步骤是直接销毁了整个组件,导致每次切换时都要重新生成新的节点,花费时间与首次挂载时一致非常长达到4s以上, FPS骤降至个位数。 file

引入KeepAlive后,除首次渲染外,切换后FPS稳定在40以上,渲染完成时间基本在1s以内; file

  1. 首次加载添加loading,保持正常FPS帧数

由于首次加载时需要渲染2000条 FormItem,其花费大量时间不可避免,当点击下一步时,会直接卡在当前步骤等待4s+直至表格渲染完成才会完成步骤切换, 给用户一种死机的感觉,用户体验非常不好。 首先为了能让点击下一步时立马响应用户操作,需要把表格数据初始化的操作设置为异步的,其次为避免长时间的表格空数据展示,添加 loading。 const initFormData = () => { setLoading(true); setTimeout(() => { const { partitionType, partitionVOList, columnInfoVOList, granularity } = tableData; form.setFieldsValue({ columnInfoVOList, partitionType, partitionVOList, granularity, }); setLoading(false); }, 0); }; 首次渲染会稍微慢些,但用户交互的及时响应对用户体验非常重要,几乎没有出现FPS掉帧现象。 file

  1. 避免同时调用大量接口

当第一次挂载时,会直接请求当前列表中所有字段的匹配标准列表,如果200个字段,就直接发送200个请求。 file 优化后: 仅在当前表单 Focus 且未缓存任何数据时才去请求接口, 忽略无用接口请求,加入防抖。 <AutoComplete value={value} onChange={onChange} onSelect={onSelect} onFocus={() => { if (!matchFieldData?.length) getMatchFieldData(value); }} /> const getMatchFieldData = (columnNameCn: string) => { if (!columnNameCn) { setMatchFieldData([]) return } const requestId = Symbol('requestId'); lastRequestIdRef.current = requestId; API.columnNameCnMatch({ columnNameCn }).then((res: any) => { if (res.success) { requestId === lastRequestIdRef.current && setMatchFieldData(res.data || []); } }); }; const debounceGetMatchFieldData = useDebounce(getMatchFieldData, 300); 5. Table复用Cell

由于 table组件的 columns如果添加了 render 自定义渲染, 那么每次父组件的 render ,会造成所有 Cell 进行 rerender,我们需要添加 shouldCellUpdate属性进行 Cell 的缓存。 目前表格表单中依赖的外部状态只有 columnTypeList字段类型的下拉列表,那么 Cell 是否需要刷新仅在字段的顺序变更,删除时需要重新进行 FormItem的注册。 那么我们可以以当前字段的 key 和位置是否变化来决定是否要重新渲染。 <Table columns={getColumns() .map((item) => { return { ...item, shouldCellUpdate: (prev: any, curr: any) => prev.key !== curr.key || prev.index !== curr.index || item.shouldCellUpdate?.(prev, curr), }; })} /> 针对需要消费 columnTypeList 状态的列,自行实现 shouldCellUdpdate, 通过 usePreviousState来实现比较 columnTypeList 是否变化。 { title: '字段类型', key: 'columnType', shouldCellUpdate: () => columnTypeList !== prevColumnTypeListRef.current, }

总结

袋鼠云数栈UED团队以数据流重构为基本出发点,对组件进行重新设计与组装,使数据模型可维护性与可拓展性大大提升。通过统一依赖管理,使复杂的表单交互逻辑祛繁就简,可溯源、易管理。并且,通过大量的技术手段,优化了性能表现,提升了用户交互体验。 对数据资产平台感兴趣的朋友,可以点击文末的阅读原文进行免费产品试用。 《行业指标体系白皮书》下载地址:https://www.dtstack.com/resources/1057?src=szsm

《数栈产品白皮书》下载地址:https://www.dtstack.com/resources/1004?src=szsm

《数据治理行业实践白皮书》下载地址:https://www.dtstack.com/resources/1001?src=szsm

想了解或咨询更多有关大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=szcsdn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/885632.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

链表的基础知识

文章目录 概要整体架构流程 小结 概要 链表是一种常见的数据结构&#xff0c;它通过节点之间的连接关系实现数据的存储和访问。链表由一系列节点&#xff08;Node&#xff09;组成&#xff0c;每个节点包含数据和指向下一个节点的指针。链表的特点是物理存储单元上非连续、非顺…

Qt的互斥量用法

目的 互斥量的概念 互斥量是一个可以处于两态之一的变量:解锁和加锁。这样&#xff0c;只需要一个二进制位表示它&#xff0c;不过实际上&#xff0c;常常使用一个整型量&#xff0c;0表示解锁&#xff0c;而其他所有的值则表示加锁。互斥量使用两个过程。当一个线程(或进程)…

网络编程,端口号,网络字节序,udp

前面一篇我们讲了网络的基础&#xff0c;网络协议栈是什么样的&#xff0c;数据如何流动传输的&#xff1b;接下来这篇&#xff0c;我们将进行实践操作&#xff0c;真正的让数据跨网络进行传输&#xff1b; 1.网络编程储备知识 1.1 初步认识网络编程 首先我们需要知道我们的…

Java基础 3. 面向对象

Java基础 3. 面向对象 文章目录 Java基础 3. 面向对象3.1. 面向对象3.2. 对象的创建和使用3.3. 封装3.4. 构造方法3.5. this关键字3.6. static关键字JVM体系结构 [^现阶段不用掌握]3.7. 单例模式 [^初级]3.8. 继承3.9. 方法覆盖3.10. 多态3.11. super关键字3.12. final关键字3.…

你的虚拟猫娘女友,快来领取!--文心智能体平台

文章目录 一、引言二、赛事介绍2.1 简介2.2 比赛时间2.3 大赛具体链接2.4 第一期赛题 三、智能体创建流程3.1 进入文心智能体平台3.1 创建智能体3.1 虚拟猫娘女友特性3.1 智能体调优 四、引言智能体测试五、结语 一、引言 我是热爱生活的通信汪&#xff0c;今天这篇博文记录一…

[CSP-J 2022] 解密

题目来源&#xff1a;洛谷题库 [CSP-J 2022] 解密 题目描述 给定一个正整数 k k k&#xff0c;有 k k k 次询问&#xff0c;每次给定三个正整数 n i , e i , d i n_i, e_i, d_i ni​,ei​,di​&#xff0c;求两个正整数 p i , q i p_i, q_i pi​,qi​&#xff0c;使 n …

C语言 | Leetcode C语言题解之第448题找到所有数组中消失的数字

题目&#xff1a; 题解&#xff1a; int* findDisappearedNumbers(int* nums, int numsSize, int* returnSize) {for (int i 0; i < numsSize; i) {int x (nums[i] - 1) % numsSize;nums[x] numsSize;}int* ret malloc(sizeof(int) * numsSize);*returnSize 0;for (in…

“2024光明多多垂直农业挑战赛”决赛启动成功举办

由光明食品集团所属上花集团的光明花博邨基地&#xff0c;与拼多多携手&#xff0c;联合中国农业大学、浙江大学等共同举办的“2024光明多多垂直农业挑战赛暨第四届多多农研科技大赛”于9月20-21日正式启动决赛。来自上海交大、中国农大、上海农科院、国家农业智能装备工程技术…

基于Node.js+Express+MySQL+VUE科研成果网站发布查看科研信息科研成果论文下载免费安装部署

目录 1.技术选型‌ ‌2.功能设计‌ ‌3.系统架构‌ ‌4.开发流程‌ 5.开发背景 6.开发目标 7.技术可行性 8.功能可行性 8.1功能图 8.2 界面设计 8.3 部分代码 构建一个基于Spring Boot、Java Web、J2EE、MySQL数据库以及Vue前后端分离的科研成果网站&#xff0c;可…

Unity 2D RPG Kit 学习笔记

学习资料&#xff1a; B站教学视频&#xff1a;https://www.bilibili.com/video/BV1dC4y1o7A5?p1&vd_source707ec8983cc32e6e065d5496a7f79ee6 2D RPG Kit Documentation.pdf文档 1、2D RPG Kit Documentation文档 1.1、Scenes/TitleScreen 开始菜单工程 1.2、https://it…

铨顺宏科技携RTLS+RFID技术亮相工博会!

中国国际工业博览会盛大开幕&#xff01; 铨顺宏科技展亮点速递 铨顺宏科技展位号&#xff1a;F117 中国国际博览会今日开幕&#xff0c;铨顺宏科技携创新产品亮相&#xff0c;吸引众多参观者。 我们珍视此次国际盛会&#xff0c;将全力以赴确保最佳体验。 工作人员热情解答…

社交内容电商中的新机遇:2+1链动模式AI智能名片商城小程序

在当今的电商世界里&#xff0c;社交内容电商正蓬勃发展。这种模式基于高质量内容&#xff0c;将有着共同兴趣爱好的用户聚集起来形成社群&#xff0c;随后引导用户进行裂变式的传播与交易。无论是像微信、微博、快手、抖音、今日头条这样的平台形式&#xff0c;还是网红、“大…

【C语言指南】数据类型详解(下)——自定义类型

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《C语言指南》 期待您的关注 目录 引言 1. 结构体&#xff08;Struct&#xff09; 2. 联合体&#xff08;Union&#xff09; 3…

【机器学习】ID3、C4.5、CART 算法

目录 常见的决策树算法 1. ID3 2. C4.5 3. CART 决策树的优缺点 优点&#xff1a; 缺点&#xff1a; 决策树的优化 常见的决策树算法 1. ID3 ID3&#xff08;Iterative Dichotomiser 3&#xff09;算法使用信息增益作为特征选择的标准。它是一种贪心算法&#xff0c;信…

Python 课程20-Scikit-learn

前言 Scikit-learn 是 Python 中最流行的机器学习库之一&#xff0c;它提供了多种用于监督学习和无监督学习的算法。Scikit-learn 的特点是简单易用、模块化且具有高效的性能。无论是初学者还是专业开发者&#xff0c;都可以借助它进行快速原型设计和模型开发。 在本教程中&a…

PFC和LLC的本质和为什么要用PFC和LLC电路原因

我们可以用电感和电容的特性,以及电压和电流之间的不同步原理来解释PFC(功率因数校正)和LLC(谐振变换器)。 电感和电容的基本概念 电感(Inductor): 电感是一种储存电能的组件。它的电流变化比较慢,电流在电感中延迟,而电压变化得比较快。可以把电感想象成一个“滞后…

Tensorflow 2.0 cnn训练cifar10 准确率只有0.1 [已解决]

cifar10 准确率只有0.1 问题描述踩坑解决办法 问题描述 如果你看的是北京大学曹健老师的tensorflow2.0,你在class5的部分可能会遇见这个问题 import matplotlib.pyplot as plt import tensorflow as tf from tensorflow.keras.layers import Dense, Dropout,MaxPooling2D,Fla…

【Verilog学习日常】—牛客网刷题—Verilog企业真题—VL69

脉冲同步器&#xff08;快到慢&#xff09; 描述 sig_a 是 clka&#xff08;300M&#xff09;时钟域的一个单时钟脉冲信号&#xff08;高电平持续一个时钟clka周期&#xff09;&#xff0c;请设计脉冲同步电路&#xff0c;将sig_a信号同步到时钟域 clkb&#xff08;100M&…

长文本溢出,中间位置显示省略号

1.说明 Flutter支持在文本末尾显示溢出省略号。现在想要实现在文本中间位置显示省略号&#xff0c;这里使用的方法是通过TextPainter计算文本宽度。&#xff08;我目前没有找到更好的方法&#xff0c;欢迎大家指教。&#xff09; 2.效果 源码 1.MiddleEllipsisTextPainter …

全球IP归属地查询-IP地址查询-IP城市查询-IP地址归属地-IP地址解析-IP位置查询-IP地址查询API接口

IP地址城市版查询接口 API是指能够根据IP地址查询其所在城市等地理位置信息的API接口。这类接口在网络安全、数据分析、广告投放等多个领域有广泛应用。以下是一些可用的IP地址城市版查询接口API及其简要介绍 1. 快证 IP归属地查询API 特点&#xff1a;支持IPv4 提供高精版、…