【问题标题】:Modeling a data-store browser为数据存储浏览器建模
【发布时间】:2022-01-08 09:28:59
【问题描述】:

我有一个连接对象浏览器,我想让用户查看他们所连接的各种数据源。对象的查看器如下所示:

  • 连接:Remote.1234.MySQL(3级源)

    • 数据库:销售
      • 表格:用户
        • 字段:名称 -- CHAR(80)
        • 字段:年龄 -- INT32
      • 表格:产品
        • ...
      • 表格:购买
        • ...
    • 数据库:其他
      • ...
  • 连接:Remote.abc.ElasticSearch(2级来源)

    • 索引:库存
      • 字段:ID -- INTEGER
      • 字段:产品 -- STRING
      • ...
  • 连接:Local.xyz.MongoDB(3级源)

    • 数据库:邮件
      • 集合:用户
        • 字段:邮箱ID -- INTEGER
        • 字段:名称 -- STRING
      • 集合:文档
        • ...
  • 连接:Local.xyz.SQLServer(4级源)

    • 数据库:主要
      • 架构:公开
        • 表格:用户
          • 字段:名称 -- STRING
    • 数据库:历史
      • ...

换句话说,“源”是由已知数量的级别和每个级别的已知“名称”组成的层次结构。虽然整个层次结构是可变的,但任何给定源的层次结构将始终具有相同数量的级别和名称。什么可能是建立关系模型的好方法?我的想法是有以下几点:

连接:

  • 身份证
  • 主持人
  • (其他细节)

来源类型:

  • 身份证
  • 姓名

SourceTypeLevelMapping:

  • SourceTypeID
  • 级别(整数)
  • 姓名

ThreeLevelSource_Level1: # 例如,数据库

  • 身份证
  • 父 ID(连接 ID)
  • 姓名
  • (其他细节)

ThreeLevelSource_Level2: # 例如,表格

  • 身份证
  • ParentID (Level1ID)
  • 姓名
  • (其他细节)

ThreeLevelSource_Level3: # 例如,字段

  • 身份证
  • ParentID (Level2ID)
  • 字段名称
  • 字段类型
  • (其他细节)

然后对其他级别的层次结构执行相同操作:

  • TwoLevelSource_Level1、TwoLevelSource_Level2
  • FourLevelSource_Level1、FourLevelSource_Level2、FourLevelSource_Level3、FourLevelSource_Level4

所以基本上定义了已知的层次结构,对于我们添加的每个新源,我们会将其附加到已知的层次结构级别之一。我正在考虑的另一种方法是为每个新源创建一个新的层次结构,但是如果我们要允许访问 25-50 个源,我们将查看数百个表。

对这种分层数据建模的好方法是什么?

(另外,是的,我熟悉此处描述的用于对分层数据进行建模的现有通用方法 -- What are the options for storing hierarchical data in a relational database?How can you represent inheritance in a database? -- 以下不是重复的。)

【问题讨论】:

标签: database relational-database database-schema hierarchical-data


【解决方案1】:

关系解决方案

响应relational-databasehierarchic-data标签,后者在前者中是行人。

1.1 初步

由于要求,以及两者的区别:

  • 真正的 SQL 平台(符合标准;服务器架构,统一语言等)和
  • 假装的“SQL”程序(没有架构;在这些程序中散布一些语言;没有事务;没有 ACID 等)不符合标准,因此错误地使用了该术语,并且
  • 非 SQL

因此,我使用 RecordField 来涵盖所有可能性,而不是使用关系术语来表达关系定义。

所有可能性都得到了满足,但关系型和符合 SQL 的方法(例如 MS SQL Server)被认为是最佳方法,因为它已有 40 年的建立和成熟期,而且没有替代方案。

  • SQL 平台集合;假装“SQL”应用程序;和非 SQL 套件,标记为 DataSource

1.2 合规性

此解决方案是 100% 关系型的:Codd 的 Relational Model,而不是学术界标榜为“关系型”的不合格替代方案:

  • 它可以在任何符合 SQL 的平台上实现

  • 它具有关系完整性(它是逻辑的,超越了参照完整性,它是 SQL 和物理的);关系力量;和关系速度。

  • 所有更新交互都很简单,通过 SQL ACID Transactions。

  • 对假装的“SQL”和非 SQL 不提供任何保证。


2 解决方案

2.1 概念

感谢您作为开发人员关注数据值以及如何检索数据。但是,首先需要两个级别的定义,以支持第三个数据级别:

  1. 目录潜力
    蓝色(参考集群)。
    组织可能使用的市场上可用的 DtaSource 和定义。根据您的描述,假设为 42。

    • 我只会将此委托给developer,而不是user_admin,因为它的设置很关键(较低级别取决于它),并且它描述了物理功能和限制每个数据源。
  2. 目录实际
    绿色(识别集群)。
    组织实际签约和使用的数据源和定义。假设12。此时我们有连接地址;港口;和users。它是受约束的 CataloguePotential,直接通过 CHECKS 调用 Functions。

    此级别定义内容(实际存在的表),它不包含数据值。

    • 保持 SQL 思维方式,因为这是最谨慎的做法,因为它是一个成熟的标准,已经成熟了 40 年,因为它为我们提供了最大的灵活性:CatalogueActual 形成了 SQL Catalogue。

    • 同样,我使用术语RecordField 表示集合中的对象,而不是TableColumn,这意味着关系和SQL 含义。

    • SQL 平台
      此级别可以由查询 SQL 目录的程序自动填充。

    • “SQL”应用程序和非 SQL 套件
      由于没有目录,人口是手动的。可以通过user_admin 完成。约束将是您的程序尝试尝试查询以验证用户提供的表定义。

  3. 当前数据
    黄色(事务集群)
    user 通过他的 Connection 从 DataSources 中查询到的当前数据,用于网页。假设是,我将 user::webpage 设为中心和管理(每个连接一个 user;每个网页一个 user),而不是 OO 对象。

    • 如果 OO 对象不可靠(取决于您使用的库),或者所有用户网页中存在一组对象,则需要添加更多约束。

2.2 方法

你需要:

  1. 简单的层次结构
    一个单父层次结构,用于复制 SQL 服务器目录中的固定定义级别,以及为假装“SQL”和非 SQL 构建的目录中的可变级别。

    • Hierarchy 文档中完全定义了关系层次结构以及 SQL 实现细节。 [§ 2.2] 中给出了简单或单亲模型。
    • Root 级别(不是 Anchor)是潜在数据源
    • Leaf 级别包含数据,RecordStruct(对于允许使用的集合中的那些)。
      • 在Potential Datasource中,具有代表性,真正的RecordTypeFieldType
      • 在Actual DataSource中,它是一个实际的Record,它是RecordType的一个实例,以及一个实际的Field,它是FieldType的一个更窄的定义。
  2. 方法/结构
    为了处理Struct,它在定义上与Record 相同,并允许Struct 包含Struct,我们需要一个抽象级别,即...

    • Article
      要么是

      • Field,它是存储的原子单位,异或
      • 一个Struct,其中包含Articles
    • 这需要一个独占子类型集群,在Subtype 文档中与 SQL 实现细节一起完全定义

  3. 方法/数组
    支持ArrayFields

    • 这些是对Field 的多值依赖,因此实现为子表。
      • 对于标量,NumElement 为 1。
      • 这使得 Field 上的 Exclusive Subtype 集群成为标量冗余所必需的。

2.3 关系数据模型

这是七次迭代后的进度。它显示了表关系级别(属性级别对于内联图形来说太大了)。

  1. 假设
    JS(或其他)对象对于网页/用户来说是本地的。如果您的对象是全局对象,则需要将值表限制为 Connection。

  2. 数据模型在单个PDF 中给出:

    • 表关系级别
    • 表关系级别 + 示例数据
    • 表属性级别 + 示例数据。

2.4 符号

  • 我所有的数据模型都在 IDEF1X 中呈现,从 1980 年代初就可以使用,这是关系数据建模的唯一符号,自 1993 年以来的标准。

  • IDEF1X Introduction 是 Codd 的关系模型或其建模方法新手的必备读物。请注意,IDEF1X 模型是完整的,它们具有丰富的细节和精确度,显示了所有所需的细节,而本土模型由于不了解标准的要求,因此定义要少得多。这意味着,符号需要被完全理解。

【讨论】:

    【解决方案2】:

    这里有三个有效的 sqlite 风格的实现(一旦使用 sqlite,不强制的列类型是可以接受的,只使用整数主键来充当 rowid):

    在所有情况下,sqlite 外键 PRAGMA 设置为 true:PRAGMA foreign_keys = 1;

    简单的实现 - 每个源/级别都有一个固定表(受外键约束)

    以下设计/实现为每种类型的数据库和级别使用一个表。表使用外键相互引用以确保正确性。例如,mongo collection 不能是 mysql database 的子级。仅在connection 级别,所有数据库类型共享同一个表,但如果预期每种connection 的属性不同,则可能会有所不同。

    create table databasetype(name primary key) without rowid;
    insert into databasetype values ('mysql'),('elasticsearch'),('mongo'),('sqlserver');
    
    create table datatype(name primary key) without rowid;
    insert into datatype values ('int'),('str'); -- you can differentiate varchar if you will
    
    create table connection(id integer, hostname, databasetype, primary key(id), foreign key(databasetype) references databasetype(name));
    
    create table mysqldatabase(id integer, connectionid, name, primary key(id), foreign key(connectionid) references connection(id));
    create table mysqltable(id integer, databaseid, name, primary key(id), foreign key(databaseid) references mysqldatabase(id));
    create table mysqlfield(id integer, tableid, name, datatype, datalength, primary key(id), foreign key(tableid) references mysqltable(id), foreign key(datatype) references datatype(name));
    
    create table elasticsearchindex(id integer, connectionid, name, primary key(id), foreign key(connectionid) references connection(id));
    create table elasticsearchfield(id integer, indexid, name, datatype, datalength, primary key(id), foreign key(indexid) references mysqltable(id), foreign key(datatype) references datatype(name));
    
    create table mongodatabase(id integer, connectionid, name, primary key(id), foreign key(connectionid) references connection(id));
    create table mongocollection(id integer, databaseid, name, primary key(id), foreign key(databaseid) references mongodatabase(id));
    create table mongofield(id integer, collectionid, name, datatype, datalength, primary key(id), foreign key(collectionid) references mongocollection(id), foreign key(datatype) references datatype(name));
    
    create table sqlserverdatabase(id integer, connectionid, name, primary key(id), foreign key(connectionid) references connection(id));
    create table sqlserverschema(id integer, databaseid, name, primary key(id), foreign key(databaseid) references sqlserverdatabase(id));
    create table sqlservertable(id integer, schemaid, name, primary key(id), foreign key(schemaid) references sqlserverschema(id));
    create table sqlserverfield(id integer, tableid, name, datatype, datalength, primary key(id), foreign key(tableid) references sqlservertable(id), foreign key(datatype) references datatype(name));
    
    

    加载代表第一个表的数据:

    insert into connection(hostname, databasetype) values ('remote:1234', 'mysql');
    insert into mysqldatabase(connectionid, name) select id, 'sales' from connection where hostname='remote:1234';
    insert into mysqltable(databaseid, name) select id, 'user' from mysqltable where name='sales';
    insert into mysqlfield(tableid, name, datatype, datalength) select id, 'name', 'str', 80 from mysqldatabase where name='product';
    insert into mysqlfield(tableid, name, datatype) select id, 'age', 'i32' from mysqldatabase where name='product';
    

    尝试对数据进行无效操作:

    insert into mysqlfield(tableid, name, datatype) values (2, 'newfield', 'qubit');
    
    -- Error: FOREIGN KEY constraint failed
    

    为了漂亮地打印整个树,有必要手动连接所有涉及的表。

    类似图的实现 - 一个表代表树,另一个代表层次结构(受触发器约束)

    这里element 表用于表示树中的每个元素/节点。它的level 列明确地将每个元素分类为databasetable 等。这里使用sqlite 的rowid 作为主键,但很容易将其更改为常规id。

    在之前的实现中,使用外键来确保模型的正确性。现在触发器用于这项工作。他们决定哪个父级别接受哪个子级别,因为它允许用于相应的 dbtype - 这些规则在 element_type 表中指定。

    最后,exra 表element_properties 用于允许将额外属性附加到任何元素,例如字段类型。

    create table db_type(name primary key) without rowid;
    insert into db_type values ('mysql'),('elasticsearch'),('mongo'),('sqlserver');
    
    create table element_type(parentlevel, childlevel, dbtype, primary key(parentlevel, childlevel, dbtype), foreign key(dbtype) references db_type(name)); --not using without rowid to be able to have null parent level
    insert into element_type values
      (null, 'connection', 'mysql'),
      ('connection', 'database', 'mysql'),
      ('database', 'table', 'mysql'),
      ('table', 'field', 'mysql'),
    
      (null, 'connection', 'elasticsearch'),
      ('connection', 'index', 'elasticsearch'),
      ('index','field', 'elasticsearch'),
    
      (null, 'connection', 'mongo'),
      ('connection', 'database', 'mongo'),
      ('database', 'collection', 'mongo'),
      ('collection', 'field', 'mongo'),
    
      (null, 'connection', 'sqlserver'),
      ('connection', 'database', 'sqlserver'),
      ('database', 'schema', 'sqlserver'),
      ('schema', 'table', 'sqlserver'),
      ('table', 'field', 'sqlserver');
    
    create table element(id integer, parentid, name, level, dbtype, primary key(id), foreign key(parentid) references element(id), foreign key(dbtype) references db_type(name));
    
    create table element_property(parentid, name, value, primary key(parentid, name), foreign key(parentid) references element(id)) without rowid;
    
    -- trigger to guarantee that new elements will conform hierarchy
    create trigger element_insert before insert on element
    begin
      select iif(count(*)>0, 'ok', raise(abort,'invalid parent-child insertion')) from element_type etc join element_type etp on (etp.childlevel, etp.dbtype)=(etc.parentlevel, etc.dbtype) where (etc.dbtype, etc.parentlevel, etc.childlevel)=(new.dbtype, (select level from element ei where ei.rowid=new.parentid), new.level);
    end;
    
    -- trigger to guarantee that updated elements will conform hierarchy
    create trigger element_update before update on element
    begin
      select iif(count(*)>0, 'ok', raise(abort,'invalid parent-child update')) from element_type etc join element_type etp on (etp.childlevel, etp.dbtype)=(etc.parentlevel, etc.dbtype) where (etc.dbtype, etc.parentlevel, etc.childlevel)=(new.dbtype, (select level from element ei where ei.rowid=new.parentid), new.level);
    end;
    
    -- trigger to guarantee that hierarchy removal must respect existing elements (no delete cascade used)
    create trigger element_type_delete before delete on element_type
    begin
      select iif(count(*)>0, raise(abort,'can''t remove, entries found in the element table using this relationship'), 'ok') from element etc join element etp on etp.rowid=etc.parentid and etp.dbtype=etp.dbtype where etc.dbtype=old.dbtype and (etp.level,etc.level)=(old.parentlevel, old.childlevel);
    end;
    
    -- trigger to guarantee that hierarchy changes must respect existing elements
    create trigger element_type_update before update on element_type
    begin
      select iif(count(*)>0, raise(abort,'can''t change, entries found in the element table using this relationship'), 'ok') from element etc join element etp on etp.rowid=etc.parentid and etp.dbtype=etp.dbtype where etc.dbtype=old.dbtype and (etp.level,etc.level)=(old.parentlevel, old.childlevel) and (etp.level,etc.level)!=(new.parentlevel, new.childlevel);
    end;
    

    加载代表第一个表的数据:

    insert into element(name, level, dbtype) values ('remote:1234', 'connection', 'mysql');
    insert into element(name, level, dbtype, parentid) values ('sales', 'database', 'mysql', (select id from element where (level, name, dbtype)=('connection', 'remote:1234', 'mysql')));
    insert into element(name, level, dbtype, parentid) values ('user', 'table', 'mysql', (select id from element where (level, name, dbtype)=('database', 'sales', 'mysql')));
    insert into element(name, level, dbtype, parentid) values ('name', 'field', 'mysql', (select id from element where (level, name, dbtype)=('table', 'user', 'mysql')));
    insert into element(name, level, dbtype, parentid) values ('age', 'field', 'mysql', (select id from element where (level, name, dbtype)=('table', 'user', 'mysql')));
    insert into element_property(name, value, parentid) values ('fieldtype', 'varchar', (select id from element where (level, name, dbtype)=('field', 'name', 'mysql')));
    insert into element_property(name, value, parentid) values ('fieldlength', 80, (select id from element where (level, name, dbtype)=('field', 'name', 'mysql')));
    insert into element_property(name, value, parentid) values ('fieldtype', 'integer', (select id from element where (level, name, dbtype)=('field', 'age', 'mysql')));
    

    尝试对数据进行无效操作:

    insert into element(name, level, dbtype, parentid) values ('documents', 'collection', 'mysql', (select id from element where (level, name, dbtype)=('database', 'sales', 'mysql')));
    
    -- Error: invalid parent-child insertion
    
    update element_type set childlevel='specialfield' where dbtype='mysql' and (parentlevel, childlevel)=('table','field');
    
    -- Error: can't change, entries found in the element table using this relationship
    

    漂亮地打印树:

    create view elementree(path) as
    with recursive cte(id, name, depth, dbtype, level) as (
      select id, name, 0 as depth, dbtype, level from element where parentid is null
      union all
      select el.id, el.name, cte.depth+1 as depth, el.dbtype, el.level from element el join cte on el.parentid=cte.id
      order by depth desc
    )
    select substring('     ',0,2*depth)||name||' ('||dbtype||'-'||level||')' from cte;
    
    select * from elementree;
    -- remote:1234 (mysql-connection)
    --  sales (mysql-database)
    --    user (mysql-table)
    --    documents (mysql-table)
    --      name (mysql-field)
    --      age (mysql-field)
    

    极简 DRY 图类实现 - 一张表,只有名称代表树,只有一张辅助表

    这里再次使用element 表来表示树中的每个元素。与前一种情况不同的是,该表的信息较少,并且每个元素的类型——无论是database 还是table,都是隐式推断的,而不是由列显式确定的。通过简单地将user 添加为sales 的子代,推断user 是mysql table,一旦它是mysql 的子代database - sales,即adatabase因为它是 mysql connection 的子元素,它是 mysql 根元素的子元素。 dbtypes 是这棵树中的根元素,它们的所有子元素都被推断为这个 dbtype。

    这里hierarchypath 表用于告诉element 树中遵循的层次结构。对于用户 confort,他只需要插入一个(> 分隔的)字符串,代表层次结构路径,从 dbtype 开始。层次结构视图会将这个字符串解构为层次结构。雇佣路径的一个示例是:mysql>connection>database>table>field

    再次注意,sqlite 的 rowid 被用作表 id。请记住,仅通过select * from table;是看不到rowid的,它默认是隐藏的,需要显式选择它:select rowid,* from table;

    create table element(name, parentrowid, foreign key(parentrowid) references element(rowid));
    -- dbtypes are the root elements
    insert into element(name) values ('mysql'),('elasticsearch'),('mongo'),('sqlserver');
    
    create table hierarchypath(path);
    
    insert into hierarchypath values
      ('mysql>connection>database>table>field'),
      ('elasticsearch>connection>index>field'),
      ('mongo>connection>database>collection>field'),
      ('sqlserver>connection>schema>database>table>field');
    

    加载数据:

    insert into element select 'remote:1234',rowid from element where (name,coalesce(parentrowid,-1))=('mysql',-1); --returning rowid; -- returning only works for sqlite 3.35+
    insert into element select 'sales',rowid from element where rowid=5;
    insert into element select 'user',rowid from element where rowid=6;
    insert into element select 'name',rowid from element where rowid=7;
    insert into element select 'age',rowid from element where rowid=7;
    

    印刷精美:

    create view hierarchy(root, depth, name) as
    with recursive hierarchycte(root, depth, name, remaining) as (
      select substr(path, 0, instr(path, '>')) as root, 0 as depth, substr(path, 0, instr(path, '>')) as name, substr(path, instr(path, '>')+1)||'>' as remaining from hierarchypath
      union all
      select root, depth+1 as depth, substr(remaining, 0, instr(remaining, '>')) as name, substr(remaining, instr(remaining, '>')+1) as remaining from hierarchycte where instr(remaining, '>') > 0
    )
    select root, depth, name from hierarchycte where depth>=0;
    
    create view elementhierarchy(root, depth, name) as
    with recursive elementcte(root, depth, name, rowid, parentrowid) as (
      select name as root, 0 as depth, name, rowid, parentrowid from element where parentrowid is null
      union all
      select elcte.root, elcte.depth+1, el.name, el.rowid, el.parentrowid from elementcte elcte join element el on el.parentrowid=elcte.rowid
      order by depth desc
    )
    select root, depth, name from elementcte;
    
    create view elementree as
    with recursive elementcte(root, depth, name, rowid, parentrowid) as (
      select name as root, 0 as depth, name, rowid, parentrowid from element where parentrowid is null
      union all
      select elcte.root, elcte.depth+1, el.name, el.rowid, el.parentrowid from elementcte elcte join element el on el.parentrowid=elcte.rowid
      order by depth desc
    )
    select substring('     ',0,2*h.depth-2)||eh.name||' ('||h.root||'-'||h.name||')' from (select *,row_number() over () as originalorder from elementhierarchy) eh join hierarchy h on (eh.root,eh.depth)=(h.root,h.depth) where h.depth>0 order by originalorder;
    
    select * from elementree;
    -- remote:1234 (mysql-connection)
    --  sales (mysql-database)
    --    user (mysql-table)
    --      age (mysql-field)
    --      name (mysql-field)
    

    这里没有实现触发器,但这样做会很好。一个例子是避免插入超过允许的级别。

    将层次结构存储在视图上看到的解构形式中会更明智 层次结构,通过在插入时间而不是每个选择查询中进行解构来避免 CPU 消耗。在这里,它以这种方式与其他实现区分开来。

    这里是最后一级实体,field 没有之前实现中显示的属性。在此模型中,需要向层次结构添加一个或两个额外级别:...table>field>fieldpropertyandvalue...table>field>fieldproperty>fieldpropertyvalue,在第一种情况下,fieldpropertyandvalue 的示例将是 datatype=integer,而分离的属性和值的示例将分别为datatypeinteger。这种任何属性都是图中新节点的方法更接近 RDF 存储使用的方法。


    最后必须说明的是,可以使用专门的图形数据库,使用它们自己的查询语言,例如 neo4j 中的 cypher 和其他语言中的 sparql,但由于图形设计总体上很简单,因此关系数据库满足我们的需要。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-04-22
      • 2013-09-25
      • 1970-01-01
      • 2014-12-26
      • 1970-01-01
      • 2021-10-13
      • 2012-09-19
      相关资源
      最近更新 更多