通常表的血缘关系、ER图、异构表建模都属于差不多类型,相较于关系模型,此类更于复杂,本次展开讲一讲异构表建模的实现及过程。
发现身边事儿、聊点周奇遇,我是沈二,期待奇遇的互联网灵魂~、一起聊天吹水,探索新的可能~wx:breathingss,入圈吧!
效果
先说说思路,需求是能够表现出不同数据库连接之间的关联汇聚,类似于一个ETL的汇聚过程,但能尽量在可视化部分表现出相关的关联;
-
表类似于组的概念,列类似于连接锚点的概念,此处的思路会有一些区别
-
有关于结构化存储的json存储,涉及的内容包含
表信息
、源信息
、选择列信息
、连接信息
等,处理起来比较繁琐,因此有了转sql的处理 -
Spark SQL 作为处理端,通过源和sql的汇聚计算处理,形成新的物理表存储
组件解析
前端
以vue2作为基础进行开发,相关涉及到自定义HTML节点需要引用@antv/x6-vue-shape
@antv/x6
经过该节点的自定义,实现了列的显示,因原本x6 markup
的语法针对一些如选择、文本超长缩略显示等问题,最终选用灵活可控的这种方式进行,紧接着遇到一些问题。
1. 锚点的设置及显示交互问题
因为用的html表格,而与绘制的锚点需要进行重叠,因此对高度及相关的显示都要准确,
table-layout: fixed;
表格和列的宽度是由table
和col
元素的宽度或第一行单元格的宽度来设置的。后续行中的单元格不会影响列的宽度。- 高度的问题,需要注意的是,如果没有内部子标签,高度经常会得不到理想高度,通过增加子标签的形式,对子标签设置高度进行解决
js
.data-table {
width: 300px;
background: #fff;
table-layout: fixed;
border-collapse: collapse;
word-break: break-all;
padding: 10px;
}
tr {
td {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
// max-width: 100px;
margin: 0;
padding: 0;
section {
box-sizing: border-box;
border-bottom: 2px dashed #eee;
height: 24px;
line-height:24px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
2. 附加操作问题
本来想着可能如果涉及LEFT JOIN
,RIGHT JOIN
会导致顺序及参照问题,需要设置一个表作为主参照,另外就是全选和反选的全局操作,因此增加了此项,最开始想放在点击或者其他地方,后面就索性放在了显眼的位置。
3.锚点交互的问题
一种是如此类进行以⚪的形式一行数据两个锚点,但这种感觉绘制的有点儿多 主要采用的是长方形的锚点,左右突出,设置层级的方式,保证了交互及显示的形式,通过样式融入。
waterline-sql-builder
用以将关系数据解析成sql语法表达式
js
var SQLBuilder = require('waterline-sql-builder');
var compile = SQLBuilder({ dialect: 'postgres' }).generate;
// Compile a statement to obtain a SQL template string and an array of bindings.
var report = compile({
select: ['id'],
where: {
firstName: 'Test',
lastName: 'User'
},
from: 'users'
});
console.log(report);
//=>
//{
// sql: 'select "id" from "users" where "firstName" = $1 and "lastName" = $2',
// bindings: ['Test', 'User']
//}
转换方法
将x6产生的关系构建成sql的解析结构,从而获取sql表达式
js
//获取sql模板字符串
getSqlString() {
const { cells } = this.graph.toJSON();
if (cells.length <= 0) {
return { sql: "", tables: [] };
}
var tables = [];
const [firstNode, ...nodes] = cells.filter((it) => it.ports);
const edgs = cells.filter((it) => !it.ports);
var columns = [];
var mapTables = new Map();
var mapSource=new Map();
[firstNode, ...nodes].map((it, index) => {
const { items } = it.ports;
tables.push(it.data);
const reName = `T${(index+1)}`;
mapTables.set(it.data.tableName, reName);
// mapSource.set(it.data.tableName, reName);
const column = items
.filter((iv) => iv.data.checked)
.map((iv) => `${reName}.${iv.data.columnName}`);
// .map(iv=>`${it.data.tableName}.${iv.data.columnName}`)
columns = columns.concat(column);
});
const getTplTb = (tableName) =>
`${tableName} ${mapTables.get(tableName)} `;
var sqlBuildSql = {
select: columns,
from: getTplTb(firstNode.data.tableName),
join: [],
};
// [firstNode,...nodes].sort((a,b)=>{
const pdt = [firstNode, ...nodes];
for (let index = 0; index < pdt.length - 1; index++) {
const a = pdt[index];
const b = pdt[index + 1];
const { data } = b;
// a~b之间存在关系, 关联因为以第一个节点为依据,所以用第二个节点作为主体;
var item = {};
const links = edgs
.filter(
(it) =>
(it.source.cell == a.data.tableId &&
it.target.cell == b.data.tableId) ||
(it.source.cell == b.data.tableId &&
it.target.cell == a.data.tableId)
)
.map((it) => {
//it.source.cell==a.data.tableId
const func = (firstNode, type) => {
let columnItemA = firstNode.ports.items.find(
(iv) => iv.id == it[type].port
);
if (columnItemA) {
// item[firstNode.data.tableName]=columnItemA.data.columnName;
const name = mapTables.get(firstNode.data.tableName);
item[name] = columnItemA.data.columnName;
}
};
func(a, "source");
func(a, "target");
func(b, "source");
func(b, "target");
});
//port
if (!Object.keys(item).length > 0) {
return;
}
var joinStr = {
// from: data.tableName,
from: getTplTb(data.tableName),
on: [item],
};
sqlBuildSql.join.push(joinStr);
}
//解析返回
var sqlStr = SQL.generate(sqlBuildSql);
//补充表源信息
sqlStr.tables = tables;
sqlStr.sql=sqlStr.sql.replaceAll('`','');
return sqlStr;
},
服务端
处理的本质其实在于有点儿类似于labda表达式那种,不同源的数据需要获取,再根据sql语法解析关联获取到实际的数据内容,然后再建表入库操作。