GraphDatabase在关系数据库中的实现
1.前言
近几年,随着WEB的发展,大家意识到传统关系数据库的不足,于是各种适用于WEB应用的非关系型数据就应运而生了.如 Cassandra, MongoDB等.也就是所谓的NOSQL数据库.
而另一方面程序员期望的面向对象型数据库,却还远不成熟,迟迟未能出现一件像样的产品,原因各种各样,但最大的问题可能是难度太大,其实面向对象型数据库差不多伴随面向对象语言的发展,在很早就已经出现了.但它有太多的晦涩和局限之处,有兴趣大家可以去研究一下,但是它在第二轮数据库大战中输给了关系数据库。
传统的关系型数据库作为数据存储的首选工具地位在短期内无法撼动。以这种数据库为基础发展起来的工具非常之多,历史也非常久远。而且关系数据库也尝试加入一些面向对象的特性,如音频、图像等新数据类型、自定义数据类型以及重载运算符等等。而Postgresql在面向对象方面更进一步,加入了如继承等特性, 一般称之为ORDBMS。
在RDBMS的使用者也从另一面方尝试解决RDBMS与应用之间的一些模式映射匹配问题,近几年出现了ORM技术,为RDBMS与面向对象语言之间减少了一些隔壑,虽然并没有从根本上解决问题,但作了一些很好的尝试,也获得了广泛的应用.但使用ORM技术,通过 Hibernate(或其他 ORM 工具)访问RDBMS,映射问题也无法彻底解决,它们只会转移到配置文件。而且,有以下问题。
1. 如果您想要创建一个分层良好的继承模型,将它映射到表或一组表无疑会是失败之举。若用违背常规形式的方式来换取查询的性能,就会将 DBA 与开发人员在某种程度上对立起来。面向对象中的继承也不能过于频繁的使用,而且不易于使用.例如你不可能让所有的实体类继承于同一个类.
2. 关系不作为实体出现, 实体之间的关系简化为依赖这一种.无法为实体之间的关系建立一个分层良好的继承模型,而有时关系很重要,如网络故障分析中网络对象之间的关系显得至关重要.
与此同时,还出现了一些专用某个领域的数据库如CMDB和GeoDatabase。 像CMDB,它将实体之间的关系提高到了与实体同等的地位,并提供了一个图查询的方法。可惜它并没有作为一个通用的数据库出现,它甚至不是真的作为一个数据库存在,而只是对CMDB用户来说像一个数据库而已.
我在网络中找到一种与CMDB很类似的数据库,名为GraphDatabase,其中的代表是Neo4j,可惜它不是免费的。而只能用于java语言。更为重要的是它们都是自已实现了一个存储系统,不能与RDBMS共存,可实际情况中,RDBMS是主流我们根本不可能抛弃它,因此我想基于RDBMS开发一个GraphDatabase数据库。它作为一个RMDBS的前端运行。
2.设想
我设想的GraphDatabase数据库并不想做成万能的,仅仅处理已有的RDBMS+ORM遇到的一些问题,在此我对这个数据库做了一些设想:
1. 很好的支持继承,让面向对象语言更方便的映射。但它并不是面向对象数据库。
2. 将关系提升至与实体同等的地位,并提供完整的图查询操作。
3. 鉴于RDBMS的垄断地位,它应该能与RDBMS良好的集成,基于它开发,也就是说本数据库的数据模型能够简单地映射到RDBMS,它们之间有一个简单的映射机制。
4. 数据库中的信息主要通过如下3个基本的构建块表示:
a) 条目(Item, 又叫做vertex)——从概念上来说,这类似于对象实例,拥有唯一的ID,并包含0个或多个以上的属性组(attributeGroup)。
b) 关系(relationship,又叫做edge)——它连接了两个Item,此外还具有方向和类型(RelationshipType),它实际上是条目(Item)的一个子类。
c) 属性组(attributeGroup) ——它是一组 key/Value对的集合。Item与Relationship都有零到多个attributeGroup。
鉴于第3个需求,我们必须基于RDBMS 上来开发,做一个数据库的前端,它可以嵌入在其他应用程序中,也可以独立运行。
此外,我们还应加上一些额外的可选功能,如
1. 数据的版本信息
2. 数据的状态管理
3. 数据的变更记录
4. 数据的基线功能
5. 自动的全文搜索
3.需求
在做这个数据库前我们还是先确定一下上述的第2种情况主要有那些需求.
3.1数据的定义
本数据库是用条目(item)、关系(relationship)和属性组(attributeGroup)三个概念来组织数据的,用户可以按自已的需求定义这三种对象,将数据库的数据看成一个有属性的有向图,如下
条目(item)就是图中的节点, 关系(relationship)就是图中的边。节点和边都具有属性,它们用属性组(attributeGroup)来组织。这三个对象的UML类图如下
所有用户自定义的条目(item)、关系(relationship)和属性组(attributeGroup)都必须从它们继承。
3.1.1条目(item)
一个条目(item)代表一个对象实例(如计算机,应用软件,或其它),
1. 每一个条目(item)将至少有一个唯一的Id,并充当一个Key
2. 为一个条目(item)指定一个Id.后,它可能用在任何需要Id的场合
3. 一个条目(item)具有0个或多个属性组(attributeGroup)。注意它自己不能直接包含属性。
3.1.2关系(relationship)
一个关系(relationship)表示源条目(item)与目标条目(item)之间的连接。如一个软件“运行(runs)”在一个操作系统上、一个操作系统“安装(installed)”在一个计算机上、一个故障(incident)记录“影响(affects)”一个计算机、以及一个服务使用另一个服务。关系(relationship)有下列特征:
1. 一个关系(relationship)严格的连接两个条目(item),一个是源,一个是目标,并提供关于这个关系(relationship)的信息。
2. 一个关系(relationship)是一个条目(item)的子类,并具有一个条目(item)的所有特征。如每个关系(relationship)都将有一个唯一的ID,并作为key。
3. 一个关系是有方向的,但本系统并没有为这个方向赋予什么特殊的意义。但删除是依赖于方向的,具体请见删除条目。
4. 一个关系(relationship)具有0个或多个属性组(attributeGroup)。注意它自己不能直接包含属性。
3.1.3属性组(attributeGroup)
属性组(attributeGroup)表示一个含有描述条目(item)或关系的属性(注意这里的属性是类似于数据库表中的字段,而不是像C#语言中的属性)的集合。属性组(attributeGroup)有下列特征:
1. 一个属性组(attributeGroup)必须关联于一个条目(item)或关系(relationship)
2. 一个属性组(attributeGroup)可能含有用于标识条目(item)或关系(relationship)的属性,或它可能含有描述条目(item)或关系(relationship)的属性
3. 不同类型的几条属性组(attributeGroup)可能关联相同的条目(item)或关系(relationship)
4. 一个属性组(attributeGroup)可能含有多个属性,也有可以不含属性,这时它充当一个标记。
一个属性组(attributeGroup)类似于SQL视图中的一行。它是一个属性的投影。相同的属性可能出现在同一个条目(item)或关系的多个属性组(attributeGroup)中。属性组(attributeGroup)可能没有属性,这种情况下它用于充当一个标记。
每个属性组(attributeGroup)可能有下列描述属性组(attributeGroup)自身的元属性
1. 有一个Id在它所关联的条目(item)或关系(relationship)的范围内中唯一的,并充当key(如果条目(item)或关系(relationship)只有一个记录时是可选地)
2. 记录的日期/时间是最后修改时间(可选的)
为什么提供属性组(attributeGroup)这样一个东西,而不是直接让条目(item)或关系(relationship)拥有属性呢?
因为真实世界中一个对象从不同角度来看它可能有不同的属性,如一个计算机,从网管员的角度看,它有主机名,IP地址,MAC址址,CPU型号,MEM大小, 硬盘容量等属性,而对财务部门的角度来看,它有订单号,单价,购买日期,使用年限,维护人等信息,
如果我们提供属性组(attributeGroup)这个概念后,用户在建立模型时可以从不同角度对实物建模,一个条目(item)可以有一到多个属性组(attributeGroup),每个属性组(attributeGroup)针对一个或多个使用者.
3.1.4继承
以上三个对象都像类一样可以继承,用户可以用它们来对自已的领域建模,它们继承的行为如下:
属性组(attributeGroup)的继承非常类似于普通的类,当一个属性组(attributeGroup)继承另一个属性组(attributeGroup)时,就具有它的所有属性,但它没有方法。属性不能重载。
条目(item)的继承则表示: 当一个条目(item)继承另一个条目(item)时,就具有它的所有属性组(attributeGroup).此外它还有二个特殊的地方:
1. 当一个条目(item)继承另一个条目(item)时,子条目(item)中有一个属性组(attributeGroup), 该属性组在子条目(item)重复申明了, 或该属性组(attributeGroup)也有一个父属性组(attributeGroup),它的父属性组(attributeGroup)已经存在于父条目(item)中,那么子条目(item)中的该属性组(attributeGroup)覆盖父条目(item)中的父属性组(attributeGroup),
2. 当出现上述情况,同时子条目(item)的约束与父条目(item)不一致时,需要注意, 父条目(item)中的约束必须比子条目(item)中的约束更严格.
这里说约束是指条目与属性组的约束.
关系(relationship)是条目(item)的一个子类,因此它的继承行为与条目(item)的继承行为完全一致。
3.1.5数据类型
Integer 通过指定范围来确定是int8, int16, int32,int64
Numeric 用户指定精度
Money 专用于存储货币类型的数据
String, 用户指定长度来确是是 char, varchar还是text
date/timestamp/duration
boolean
ipAddress ip地址,包括ipv4和ipv6
physicalAddress MAC地址
GUID类型
XML 数据
数组
在这里我就不详细说了,将会在概要设计中说明.
3.1.6约束
在描述约束之前我们说明一下约束更严格的含义: 约束a比约束b更严格,意味着假如一个实例满足约束a,那么该实例一定满足约束b,反之不一定成立。
约束分为三种:一种是针对属性的值约束,一种是条目与属性组的约束,一种是针对条目与条目的关系约束。
3.1.6.1属性的值约束
它主要是针对单个值的限定,它是我从xml的限定学过来的
限定 |
描述 |
enumeration |
定义可接受值的一个列表 |
fractionDigits |
定义所允许的最大的小数位数。必须大于等于0。 |
totalDigits |
定义所允许的阿拉伯数字的精确位数。必须大于0。 |
length |
定义所允许的字符或者列表项目的精确数目。必须大于或等于0。 |
maxExclusive |
定义数值的上限。所允许的值必须小于此值。 |
maxInclusive |
定义数值的上限。所允许的值必须小于或等于此值。 |
maxLength |
定义所允许的字符或者列表项目的最大数目。必须大于或等于0。 |
minExclusive |
定义数值的下限。所允许的值必需大于此值。 |
minInclusive |
定义数值的下限。所允许的值必需大于或等于此值。 |
minLength |
定义所允许的字符或者列表项目的最小数目。必须大于或等于0。 |
pattern |
定义可接受的字符的精确序列。 |
这些限定都与类型相关,具体不再叙述了,将会在概要设计中说明.
3.1.6.2条目与属性组之间的约束
它主要是描述一种条目可以包含的那些属性组可以出现的次数,可以用以下两个限定符来修饰属性:
maxOccurs 表示最大出现次数,默认值为1
minOccurs 表示最小出现次数,当为0时,表示可选,默认值为0
这两个修饰属性的值必须是一个正整数或"unbounded","unbounded"表示不限制。
注意,关系是条目的一个子类,因此它一样也有此约束。
3.1.6.3条目与条目之间的关系约束
它主要是描述一种关系中源条目或目标条目可以出现的次数。出现次数可以用上面的两个限定符。
maxOccurs 表示最大出现次数,默认值为unbounded
minOccurs 表示最小出现次数,当为0时,表示可选,默认值为0.
这两个修饰属性的值必须是一个正整数或"unbounded","unbounded"表示不限制。需要注意的是一个关系有两个端点,需要分别对这两个节点分别用这两个限定符进行修饰。
3.2数据操作
3.2.1一般功能
对数据库的操作无非就是插入,更新,删除和查询, 其中最重要的就是查询了.
3.2.1.1查询
3.2.1.1.1普通查询
基本上与常见的ORM工具提供的查询语言(hibernate的HQL或)没有什么区别,一般Select 语句能支持的都支持,在这里我就不再说了。具体的设计将在概要设计中定义.
注意我们不要开发一个像HQL那样的查询语言,而是应该用一个SQL语句的抽象类库来生成数据库的原生SQL语句。
3.2.1.1.2图查询
这里就是与其他ORM工具提供的查询语言(hibernate的HQL)不同的地方了,它提供了完整的图查询操作。
在设计一个图查询之前我们想象一下我们对一个图进行查询对有什么样子的需求呢
1.从一点或n点出发,走指定的条件的线路,找出所有可到达的所有端点和线路
2.从一点或n点出发,走任意线路,找出所有可到达的所有端点和线路,但这些端点必须符合指定的条件。
3.从一点或n点出发,走指定的条件的线路,找出所有可到达的所有端点和线路,但这些端点必须符合指定的条件。
4.以上三个反过来,反过来查起始端点
因此我们将图查询设计为由三部分组成,源条目过滤表达式,目标条目过滤表达式和关系过滤表达式。其中源条目过滤表达式和目标条目过滤表达式在格式上完全相同,我们称之为条目过滤表达式(itemFilter),而关系过滤表达式(relationshipFilter)则稍有不同,它是在条目过滤表达式的基础上增加了一个遍历深度参数,你可以认为关系过滤表达式(relationshipFilter)是条目过滤表达式(itemFilter)的派生类。
其中源条目过滤表达式,目标条目过滤表达式是可选的,但不能相同两个都没有。
条目过滤表达式(itemFilter)
一个条目(item)匹配一个itemFilter当且仅当下列规定所有都为真时:
1.该条目符合定义在itemFilter中的约束。
2.当它作为源条目过滤表达式时,都有一个匹配 relationshipFilter 并将此条目(item)作为源的关系。
3.当它作为目标条目过滤表达式时,都有一个匹配 relationshipFilter 并将此条目(item)作为目标的关系。
虽然关系也是一个条目,但条目过滤表达式(itemFilter)不会返回关系实例。
关系过滤表达式(relationshipFilter)
一个关系匹配relationshipFilter当且仅当下列规定所有都为真:
- 符合 relationshipFilter中的约束的关系。如果源条目到目标条目之间要经过多个端点时,我们可能需要增加一个针对中间端点的itemFilter。
- 关系的源条目(item)匹配源条目过滤表达式。
- 关系的目标条目(item)匹配目标条目过滤表达式。
- 图中源条目和目标条目之间的边的数量满足指定的条件。
没有一个源或目标的关系,不能匹配relationshipFilter。
通过这三个部分的组合,基本上可以达到上面提到的要求了,便几点需要注意:
1.一个图中可能会有一个环,用户无需关心,实现本文的实现应该自己处理
2.因为图查询其实是一个递归操作,因此需要对递归的深度进行限制。
3.一个端点可能会有多个到达另一个端点的路径,只要这些路径符合relationshipFilter,那么它们就应该出现在结果中。
3.2.1.2插入
属性组的插入, 可以将它当作表一样,基本上与常见的Insert支持的都支持,在这里我就不再说了。但需要注意满足条目与属性组的约束。
条目的插入,它有点特殊,因为有条目与属性组的约束和条目与条目的关系约束,因此你必须一次性地将条目和条目的必选属性组以及条目与其它条目的必选关系一起建起来,否则无法创建成功。可能还要一同创建必要的子条目。
关系的插入,它也有点特殊,因为有条目与属性组的约束,因此一次性的将关系与它的必选属性组一起建起来,否则无法创建成功。
3.2.1.3删除
这个就是与一般的数据库相差甚远,因此重点说明一下。
3.2.1.3.1删除属性组
基本上与常见的ORM工具提供的删除没有什么区别,一般Delete 语句能支持的都支持,在这里我就不再说了。当然删除它不能违反条目与属性组的约束。
3.2.1.3.2删除条目
删除一个条目时必须同时删除该条目的所有属性组,但在删除它之前,需要同时删除与它相连的关系。但删除关系时需要满足下面的条件:
l 不能违反条目与条目之间的关系约束
在实际情况中,仅仅这样子是不够,你可能希望在删除一个条目时,将其相邻的条目删除(相邻是指两者之间有一个关系存在),但这需要清楚该条目能否安全的删除。所以我们需要用户来指定一下:
在定义关系时,在定义关系的源和目标时各增加一个onDelete字段,用来表示删除这个关系时,是否连带删除该条目.
呵呵,这个就是数据库的级联删除了.
3.2.1.3.3删除关系
同样,删除一个关系时必须同时删除该关系的所有属性组。注意同样地删除时不能违反条目与条目的关系约束。
3.2.1.4更新
属性组的更新,可以将它当作表一样,基本上与常见的Update支持的都支持,在这里我就不再说了。
条目的更新,它有点特殊,因为它的属性都归类在属性组中,自身没有属性,那么唯一可以更改的就是改变类型了。但改变类型需要注意,两个类型可以拥有的属性组是不一样的,你可能在更改它的类型时,需要删除和添加一些属性组。
关系的更新,因为它是条目的子类,因此它的更新与条目的更新差不多,但它比条目多两个属性,即源条目和目标条目的引用。源条目是不可更改的,但目标条目的引用是可以更改的。用户可以修改它,但要注意不能违反条目与条目的关系约束
3.2.2可选功能
3.2.2.1对象的状态管理
一个对象在它的生命周期中可能会经过几个阶段, 而对不同的用户来说在对象处于某些阶段时他并不想看到它,如一个PC可能会分为购入待处理、试用、正式运行、停用、作废。对财务人员来说,从它生命周期开始到结束之前都能看到。而对网络管理的值班人员来说只希望看到处于“试用”和“正式运行”阶段的PC。
针对这种情况本数据库引入一个环境的概念,即管理员可以为系统定义几个环境,并定义这些环境可以访问到处于哪几个状态的对象,并且为用户设置一个默认环境。用户可以在运行中更改自己所处的环境。
3.2.2.2数据的版本管理
每一个数据我们都给它一个版本,初始值为0,以后对数据的每次变化时,都给它一个新的版本号,以便用户进行调试或查询。
3.2.2.3数据的变更记录
对数据的每次变更都记录下来,形成一个历史记录,以便用户进行调试或查询。它可以与基线管理配合使用。
3.2.2.4数据的基线管理
将数据的某个版本作为一个基线,以后每次的变更都是针对这个基线的增量。
3.2.2.5数据的全文搜索
3.3非功能性需求
3.3.1分布式的需求
本数据库暂时对分布式方面的要求不高,或者说暂时不需要,在第一个实现中不与考虑.
3.3.2数据库事务的要求
虽然本数据库对分布式要求不高,但对事务时要求却很高,必须有完整ACID支持,幸运的是我们有下层的RDBMS来保证,我们只要提供封装就可以了。
3.3.3移植的要求
这个分为两个方面
一、对RDBMS数据库的移植要求,暂时不要求支持多种数据库,仅支持PostgreSQL.
二、对操作系统方面的移植要求,暂时要求支持windows和redhat。
4.设计思路
本数据库的架构大致如下
整个数据库分为两大部分:Model generator负责将用户创建的数据类图生成为多个RDBMS表或视图的创建或修改语句。而DB front-end 则负责将客户端的请求转化为一个或多个SQL语句。总之尽量让RDBMS来完成工作.
那么本数据库的对象模型如何映射到RDBMS上呢?在说明这个之前我们先来介绍一下PostgreSQL数据库的两个功能:
一、表的继承
二、递归查询,或者叫公共表表达式 (Common Table Expression,CTE)
现在聪明的你差不多应该明白我的想法了, 我们按数据的定义中的UML类图中定义的那样子在RDBMS中定义5个基本的表,其它用户定义的派生条目(item) 、派生关系(relationship)和派生属性组(attributeGroup)则各对应一个表,所有的这些表的继承均与类的继承关系完全一致。
4.1模型的映射
我将它简化一下仅保留item、attributeGroup和relationship, 如下
CREATE TABLE identifierObject ( id INTEGER PRIMARY KEY ) ;
CREATE TABLE itemObject ( ) INHERITS (identifierObject);
CREATE TABLE item ( ) INHERITS (itemObject);
CREATE TABLE relationship (
id INTEGER PRIMARY KEY,
source INTEGER NOT NULL REFERENCES item (id) ON UPDATE CASCADE ON DELETE CASCADE,
destination INTEGER NOT NULL REFERENCES item (id) ON UPDATE CASCADE ON DELETE CASCADE,
UNIQUE(source, destination)
) INHERITS (itemObject);
CREATE TABLE attributeGroup (
id INTEGER PRIMARY KEY,
ownerid INTEGER NOT NULL REFERENCES item (id) ON UPDATE CASCADE ON DELETE CASCADE
) INHERITS (identifierObject);
CREATE INDEX a_idx ON relationship (source);
CREATE INDEX b_idx ON relationship (destination);
CREATE INDEX c_idx ON attributeGroup (ownerid);
-–确保它不与自己发生关系
ALTER TABLE relationship ADD CHECK (source <> destination) ;
现在模型建立好了,我们可以开始对它进行操作了.
4.1.1创建
这个不用说了吧
4.1.2删除
删除一个条目
DELETE FROM item WHERE id = 2; --因为相关的relationship和attributeGroup有CASCADE选项,与之相关的关系和属性组会自动删除
删除一个关系
DELETE FROM relationship WHERE id = 3;
删除一个属性组
DELETE FROM attributeGroup WHERE id = 6;
4.1.3更新
这个不用说了吧
4.1.4查询
呵呵,普通查询我就不说了,我们说说图查询吧.
4.1.4.1简单一级的查询
查询id为1的条目相邻的条目
SELECT *
FROM item n
LEFT JOIN relationship e ON n.id = e.source
WHERE e.id = 1; -- 查询id为1的条目相邻的条目
4.1.4.2复杂一点的递归查询
从id为1的条目出发,询它沿关系能到达的条目,并返回路过的中间节点的个数和路径
WITH RECURSIVE transitive_closure(source, b, distance, path_string) AS
(
SELECT source, destination, 1 AS distance,
source || '.' || destination || '.' AS path_string
FROM relationship
WHERE source = 1 -- source
UNION ALL
SELECT tc.source, e.destination, tc.distance + 1,
tc.path_string || e.destination || '.' AS path_string
FROM relationship AS e
JOIN transitive_closure AS tc ON e.source = tc.destination
WHERE tc.path_string NOT LIKE '%' || e.destination || '.%'
)
SELECT * FROM transitive_closure
4.2讨论
我对postgresql进行过测试,在这种继承结构下,它是很低效的,一般来说你对一个父表进行查询时,它会依次对派生表进行查询的,当派生表太多时,它的查询时候基本上变成了你查询每一个表的时间总和的三分之一(这好像是依赖于查询的并发数)。此外继承还有一定的局限性。
因此我想既然item表中没有用户定义的属性,那么父条目与子条目的属性是相同的(有一些系统定义的属性),那么条目(item)的继承我们可以不用表继承的方式实现,而是加入一个表示它的类型的字段。当你查询指定类型的条目时,可以在where中加上一个 type = x的过滤表过式,而这个表示类型的字段的设计请参见层次型枚举。
将对象的类型改成一个字段来表示,还有一个好处就是用户可以更改对象的类型,而这样子更符合面向对象的思想。
5.后记
呵呵,这个数据库是我为公司设计一个CMDB原型时开始构思的,花了8天完成了本文。其间对数据库了解从仅知道简单的Select到了解分区、物化视图、自定义操作符、公共表表达式(Common Table Expression,CTE)。感谢Google,让我可以查到大量的资料。
runner.mei
2010.9.14
posted on 2010-09-15 09:34 runner.mei 阅读(2006) 评论(3) 编辑 收藏 举报