干货精华 | Tapdata 开源教程之异构数据库模型推演

2023-01-17 数据库云数据库 SQL ServerSQL单元测试MongoDB

自开源以来,tapdata 吸引了越来越多开发者的关注。在和社区成员讨论共创的过程中,我们也意识到在基础教程之外,补充更多原理解析的重要性和必要性。为了辅助开发者更好地理解 tapdata community 的技术逻辑,真正实现快速理解、深度参与,我们特别增加了 tapdata 功能特性及原理解读教程。 本期主题为「异构数据库的模型推演」,核心内容包括::

异构数据库模型推演关键名词解释异构数据库模型推演核心原理解析模型推演的可维护性保障

01

什么是异构数据库模型推演

关键名词解释

异构数据库模型推演:用以解决异构数据库间数据同步时目标库数据类型的“最佳选择”问题。taptype:tapdata 提供的中间数据类型,可在数据同步之前,将所有数据库类型转换成中间数据库类型,是介于源库和目标库之间的“中转站”。模型推演算法:采用算分机制进行类型排序, 并返回最匹配数据类型,这个算法可以做到相对稳定。模块单元测试:模型推演可维护性的解决方法,用以保障模型推演的可持续发展。

为什么需要异构数据库模型推演?

以 mongodb 到 mysql 的数据同步为例:

mongodb 的数据类型

mysql 的数据类型

如上图所示,两个数据库之间的数据类型明显不同。假设现要将 mongodb 中存在的 _id 数据 objectid、企业名称、企业创建日期、员工人数同步到 mysql,就需要选择 mysql 所适配的数据类型:

mongodb 示例库表推演出 mysql 的建表语句

其中,都是24位的 objectid 可以适配字符串 varchar(24);tapdata 这个名称对应 varchar(100),mongodb 的 string 实际上很长,这里出于经验考量会为其选择一个相对小一点的方式来存储这个字符串,如果在使用过程中发现不够长,也支持修改;企业创建日期属于日期类型,对应到 mysql 就是 datetime(3),毫秒位的精度类型;员工人数则是从 mongodb 的 numberint 到 mysql 的 int。

以上就是我们在异构数据库数据同步过程中,所面临的工序:在目标库中,为源库数据选择对应的匹配类型→建表→插入数据。而这里还涉及到一个数据库类型的“最佳选择”问题,也就是异构数据库模型推演所要解决的问题。

02

如何完成异构数据库模型推演?

核心思想:引入中间数据类型 taptype

在将数据导入异构数据库之前,可以先将其转换为中间数据类型,也就是我们所说的 taptype。

那么我们为什么需要“创造”这样一种中间数据类型呢?

taptype:解决异构数据同步复杂度不断升级的问题

假设不存在 taptype,在进行数据同步时,所有异构数据库之间都会是直接连接的一一对照关系。随着支持的数据源和目标的数量不断增加,其间对照关系网的复杂度就会呈指数级增长。但如果我们尝试引入一个中间数据类型,先将这些数据库都用中间类型来描述,再由此实现向下游数据库类型的转换,就能很好地解决这一问题。

以下是中间类型 taptype 的设计思路

1. 通过 json key 类型表达式匹配数据库类型,json value 描述类型边界

2. 每种 taptype 类型自己专有的属性参数

3. 源库数据流入 tapdata 引擎时,会对其做一层转换,然后再根据目标库所对应的 taptype 配置对数值进行再生成,这里涉及一个值转换过程。值转换采用 mongodb 类似的 codec 设计, 提供默认 codec 和自定义 codec 实现

4. 采用算分机制作为核心算法,为原表类型匹配目标表的最佳类型

如何使用中间类型 taptype?

以“为 pdk 数据源提供 api 支持”为例:

1. 定义数据源的类型表达式以及边界描述

*表达式说明:

通过这样的方式,描述了源库所有字段的边界情况,以及如何用表达式来进行类型识别。与此同时,目标库也提供了相应的类型表达式,由此,我们就可以通过两者的关系为模型推演提供实现的基础。

在 pdk 开发中,text相当于精准匹配,这里数据库类型就叫 text,最大边界是 4gb,优先级为 2,为中间类型tapstring;bit varying[($byte)] 里的 byte是一个变量,是二进制的长度, 可以在创建表时指定,这里的[]代表可有可无,在完全没有变量的情况下,就会采用 default 值,所以这部分的最大边界是64,没有[]的情况就是 64,queryonly=true 的含义是这个类型只会用于源表读取,目标建表就不会使用它来选择建表字段;"value": [-2147483648, 2147483647]配置了最小、最大值。

2. 自定义 codec,指定特定 taptype 类型采用什么数据库类型来接收并如何接收

*表达式说明:

我们是通过这种方式,来支持开发者自定义所需要的 codec 转换的。

从创建目标库的角度来看,流入的数据是 tapmapvalue,也就是 map 值,但目标库中没有相匹配的类型可以接收,这可能就需要将其转换为一个 text,转成 tojson string 之后再 return 回去。简言之, tapmap 这样的数据过来,我们会用 text 建表,同时将其转成 json string 的方式入库。也就是说,在我们不支持某一些类型的时候,可以通过这样自定义的方式来完成值转换过程,并干预建表的字段选择。

另附类型表达式介绍文档(详见本篇附录),我们在这里详细描述了 taptype 的相关定义: https://tapdata.github.io/docs/connectors/docs/data-type-expressions.html

模型推演的实现原理是什么?

推演模型模块划分

模型推演可以大致分为以下三个模块:

1. 类型映射模块

a. package io.tapdata.entity.mapping

b. 一个特殊的 map(tapdata 定制 map,输入数据库的真实类型,就能自动匹配到表达式的边界信息),通过类型表达式匹配数据库类型作为 key 存储,并对应类型边界描述信息作为 value 存储

c. 通过 taptype 对象和类型表达式生成数据库类型

d. 每个 tapmapping 对象提供了和 tapfield 的亲和度分数计算

2. 值转换模块

a. package io.tapdata.entity.codec

b. 提供 java 类型的默认值转换codec

c. 运行通过 pdk api 扩展自定义的值转换

i. 从源库读出的特殊数据类型, 通过 totapvalue 的 codec 转换成为某一种 tapvalue 对象

ii. 从引擎过来的 tapvalue 对象可以通过 fromtapvalue 的 codec 进行转换并指定建表的数据库类型

3. 模型推演模块

a. package io.tapdata.entity.conversion

b. 通过类型映射模块和值转换模块完成该模块功能

c. 提供类型映射 api(autofill),输入数据库类型以及类型表达式 json, 就能自动生成 taptype

d. 提供类型转换 api(convert),输入原表字段列表, 目标表类型表达式 json 和目标表值转换, 就能输出目标表最佳匹配的字段列表

tapdata 类图:共五个模块,中间构成了模型推演的核心对外模块

整体层次结构上,如上图所示,四大模块(schema、event、mapping、值转换 codec)加持下,我们的模型推演模块(conversion)就可以“坐享其成”,轻松使用各大模块的功能,快速实现 autofill,帮助各数据库类型自动匹配 taptype,并根据输入的源库的字段类型、目标库的类型表达式,以及值转换相关的 codecfieldmanager,推演出最适合的目标库类型,从而达到模型推演的目的。

其他处理逻辑问题补充

关于模型推演,还有一些开发者们在使用 tapdata 或进行 pdk 开发过程中,可能会关心的非主线功能的处理逻辑问题,这里也依据大家的常见反馈,作补充说明如下:

1. 如果出现没有映射到的数据库类型, 统一采用 tapraw 去处理;

2. tapraw 在目标端如果没有特殊定义, 选择目标库最大的字符串类型接收并且按对象 tostring 做值转换(*注意:这一条特指在开发者不知道具体该如何做的情况下,我们通过找到最大字符串的办法来尽可能满足需求,但在实际操作过程中,最终结果往往不会特别好看,因此我们还是希望是大家在做类型描述时能够做到更加精准。当然,如果不可避免地出现这种情况,我们也会有日志打印出来);

3. 如果源库字段边界大于目标库所有字段时, 会选择不匹配里距离源库字段最接近的字段, 并会有警告记录;

4. 类型表达式大小写不敏感,但是对空格敏感。

模型推演算法简介

① 模型推演的算分机制

模型推演的算法采用算分机制,对各个类型的亲和度进行算分并排序,并返回最匹配类型。这个算法可以做到相对稳定,因为它将感官上的“感觉应该更好”,抽象化为数字化的结论,通过量化的方式,更方便地得到相对稳定的排序。在可维护性上,复杂度也会比写 if else 更简单。任何复杂的东西我们都应该将其抽象为简单算法,这才是真正的最佳解。

在算分机制上,我们的大致思路是依靠“权重”来处理稳定性的问题,通过在各项基准的分数上加加减减,来稳定模型推演算法。举个简单的例子:对于 tapstring, 就可以基于源库和目标库的 byte 差来衡量,差越大,亲和度越低,这是一种基准。再例如 fixed=true/false,当两个字段都是 fixed=true 时,亲和度显然就更高,两者相同就是加分项,不同就是减分项,以此为权重计量标准。除此之外,tapnumber 中 scale 描述有无小数点、是否都是 unsigned 等等,都会影响权重分值的计算。

当发现模型推演效果不佳时, 可以通过提高或者修改 pdk 数据源的 json 类型描述的准确度,快速高效地解决问题。参数配置填得越精细,匹配精度也会越高。这也是我们后续的一个发展方向——通过这细化参数配置来去提高我们模型推演的精度。我们将更详细地提供更多参数,让 pdk 开发者能够更细粒度地描述这些类行差异性。从而通过更细节的权重算分,让算法得以越来越精准、越来越精准。

验证模型推演准确性

为了验证模型推演的准确性,我们提供模型推演对照表如下:

1. 通过数据库模型对照表能更容易的发现模型推演的问题, 有助于尽早解决

2. 通过类型表达式能支持数据库类型的各种灵活写法

以 oracle 到 mysql 为例

以上表为例(oracle → mysql),我们会自动读取 pdk 的 oracle.jar 和 mysql.jar,并读出其中的类型表达式以及自定义值转换等相关干预,然后会将所有类型依次推演一遍。在这个过程中,我们会自动寻找这些变量边界的最小值和最大值以及中间值,然后自动生成一个类型,并推到目标数据库类型。这个表在这里更多扮演预览的角色,用于验证 oracle 到 mysql 的这些类型是否能推演,我们可以通过自身经验,来判断是否有出错的地方,再对应地去调整。

http://mpvideo.qpic.cn/0bc3baaaaaaa3iaigtzjujrvacgdaaeaaaaa.f10002.mp4?

详解如何生成模型推演对照表,戳这里查看

cd 到 plugin-kit 目录下,执行指令: ./bin/pdk modelprediction -o ./output ../connectors/dist/mongodb-connector-v1.0-snapshot.jar ../connectors/dist/mysql-connector-v1.0-snapshot.jar

03

如何保障异构数据库模型推演的可维护性?

综上所述,模型推演的实现无疑是一个相当复杂的过程。随着数据库类型的不断扩充,其逻辑复杂度也在不断提升,如何在这样的背景下始终确保模型推演的可维护性,也是我们不得不面对的一个问题。下面我们就站在软件工程的视角,来聊一聊我们将如何促成模型推演的可持续发展。

对于开发人员而言,如果要开发一个模块供其他人者前端使用,这个模块自开发之日起,面临着持续的功能迭代, 因此如果从起点就没有架构好,这些代码只会随着时间的流逝变得越来越不可维护。那么该如何做才能让开发的模块在半年内,1 年内乃至3年后还能平稳地增加更多功能,同时维持较低的 bug 率呢?

我们的选择是“模块单元测试”——将任何一个系统拆按分成不同的模块,每个模块都配备对应的单元测试,用以确保每个模块的输入输出是真正准确的。

以下便是我们在模型推演单元测试上的执行思路:

1. 模块初期只写主线单元测试, 把用 main 方法测试的习惯改到单元测试里, 不浪费

2. 当出现 bug 时,优先想着用单元测试的方式验证怀疑的 bug 逻辑, 即便不是这个模块的问题, 也积攒了一条测试用例

如此日积月累, 就能攒下不少单元测试用例。当某一天我们需要增加功能的时候, 会再跑一遍测试用例, 如果跑不通,存在两种可能性:

1. 新功能的改动影响到了旧功能的逻辑,单元测试发挥了最大价值, 避免被别人发现 bug

2. 可能是由于逻辑的变化, 这个测试用例跑不过是合情合理的, 那么就需要顺手把预期值修改一下, 继续保持单元测试的正确性

http://mpvideo.qpic.cn/0bc3bmab6aaamqajxizjw5rvac6dd4fqahya.f10002.mp4?

测试用例写法演示,戳这里查看

通过这样的单元测试,我们的代码变得更加稳定,各项历史功能的通畅性不因新功能的加入而有所转变,成功维护了模型推演的可持续发展。

关于模型推演的后续计划,在产品方面,后续将支持用户通过 dag 上的一个节点修改每个字段的 taptype 参数, 修改之后目标端就能立即看到为目标节点匹配的最佳数据库类型;在目标端用户可以得到我们推荐的最佳数据库类型, 同时也支持用户选择我们推荐的前5个最接近的数据库类型(按亲和度排序)。

在技术层面,确定的改进计划包括:类似于 mongodb 这样的大边界数据类型, 通过 sample 数据让数据边界更精确(目前是通过程序员经验值);考虑到不管是通过 sample 数据或者程序员经验值, 都不能完美的解决数据值边界的问题, 所以我们还将就这个问题持续优化,例如新增当数据超边界时, 自动进行 ddl 更改边界, 用户可以自主选项是否打开此功能。

【附录:taptype 类型表达式重点解析】

① 共计11种 taptype

tapboolean:布尔值tapdate:日期taparray:数组tapraw:未知类型tapnumber:数字tapbinary:二进制taptime:时间tapmap:map 值tapstring:字符串tapdatetime:日期+时间tapyear:年

有了这样些中间数据类型,在做值转换的时候,任何数据正式进入到我们的引擎之前,我们都会将原始值配合这些 taptype 定义,包装成为对应的 tapxxxvalue,也就是中间类型对应的值对象(包含了值,以及 taptype 类型):

tapbooleanvaluetapdatevaluetaparrayvaluetaprawvaluetapnumbervaluetapbinaryvaluetaptimevaluetapmapvaluetapstringvaluetapdatetimevaluetapyearvalue

而在进入目标库的 pdk connector 时,我们会先把这些 typevalue 再转换成为各自的原始值,让 pdk 的开发者把数据真实地写到目标库。

② 匹配过程

以数据类型表达式int[($bit)][unsigned][zerofill]为例:

[]代表可有可无()没有特殊含义$代表指向一个变量,到符号结尾就会自动截断成为一个变量,此处意味着 bit 可能是 int(8)、int(32) ,或是 int(64)

因此,以下这些类型,都是可以被该表达式自动映射的:

intint(8)int(32)unsignedint(64)unsigned zerofill

如此一来,就可以大大简化我们在处理类型映射时的书写复杂度。pdk 开发者们就需要用这个方式来描述我们的数据类型表达式的匹配关系。

"to"是我们通用的一个最重要的 value 部分,需要由此来表明这个表达式应该去到什么样的 taptype 类型,属于必填关键字段。

其他通用字段解析:

{
  "name" : "typename", //optional, name of data type, will display to users. if not specified, name will be generated automatically by removing all variables. 
  "queryonly" : true, //optional, default is false. the type is only for query, will not be used for table creation. 
  "priority" : 1 //optional, default is integer.max_value. if source type matches multiple target types which is the same score (bit or bytes), then the target type will be selected when the priority is the smallest.
  "pkenablement" : true //optional, default is true. whether the data type can be primary key or not. 
}
"name":给表达式起别名。在命名方面,我们会通过自动拆解表达式,默认 int 作为名字存在。但如果表达式写得比较复杂,拆解之后可能会变得不好看。这时就可以选择利用name自定义一个别名; "queryonly" : true:任何 taptype 类型都适用,代表这是一个只用于查询的类型,即只会在查询时参考这些信息,在建表时则不会采用这个字段类型。例如在数据库中,存在一些聚合字段,或是一些不常用的字段、过期字段等,不希望将其用于建表,就可以通过这个方式过滤掉;"priority":现阶段用得并不是太多,大致了解即可,主要用于表示所有其他参数完全一致的情况下会优先选择谁; "pkenablement":这是一个关键参数,表意为“能不能做主键”,在建表时,基于经验,我们会知道哪些适合建主件,而哪些不适合。这样的情况下,就需要我们给这些类型分别做上标记。在选择类型时,如果是主键,我们会选择"pkenablement" : true的那个类型去做推演运算; 更多非通用字段解析,详见完整版视频回放,搭配文档食用效果更佳哦。 https://tapdata.github.io/docs/connectors/docs/data-type-expressions.html

关于推演结果准确度

匹配优先级时的采信顺序为:value/unsignedvalue > bit > precision

事实上,众多参数中,除了"to"是必填项,其他都可选填,但大量不填的直接后果就是推演到目标类型的时候不精准。换言之,开发者输入的信息越多越精准,推演结果也就越精准。因此这里也要求开发者根据自身对数据库类型的理解,尽可能完成相关参数的精准填写。

github 项目链接: https://www.github.com/tapdata/tapdata

上一篇:电流电压超前滞后的几个动图

下一篇:Tapdata x Eoapi 插件上线!让数据真正的流动起来,API 管理更方便!