1 模型和目标
建模是为了让不规则的领域的一些具体方面变成结构化的、可操纵的空间。对于事物实际存在的方式,并没有一种天然的表达方式,我们只能有目的地选择、抽象和简化,一些方法能更好的满足某个特定目标。
图建模与其他建模技术的不同之处在于其逻辑模型和物理模型之间有更加密切的关系。关系型数据管理技术背离了用自然的语言来描述领域:
- 先将其表述成逻辑模型,然后生硬的将其转换成物理模型
- 转换使得概念化的世界和模型的数据库实例之间产生了语义失调
- 图数据库建模这个分歧会明显的缩小
2 带标签的属性图模型
带标签的属性图模型主要有以下几个特征:
- 由节点、联系、属性和标签组成
- 节点上包含属性
- 节点上可以被打上一个或者多个标签
- 联系连接节点
- 一个方向
- 一个名字
- 一个开始节点
- 一个结束节点
- 联系也可以有属性
- 可以给图算法提供元数据
- 给联系增加额外的语义(包括特权和权重)
- 可以用于运行时的约束查询
3 查询图:Cypher简介
3.1 Cypher的理念
这个模式描述了3个有交集的朋友,用ASCII字符画表达出来就是:
(emil)<-[:KNOWS]-(jim)-[:KNOWS]->(ian)-[:KNOWS]->(emil)
这个模式描述了一条路径,它将一个叫jim的节点和两外两个叫ian和emil的节点连接起来,同时也将ian节点和emil节点连接起来。
要注意:ian、jim和emil是标识符。标识符可以让我们在描述一个模式时,多次指向同一个节点(可以帮我们绕过查询语句其实只有一个方向的事实【只能从左到右处理文本】),而示意图可以从两个防线展开。除了偶尔需要使用这种方式重复使用标识符,整个语句的意图仍然是清晰的。
示意图一般倾向于使用特定节点或联系的实例,而不是类或原型。即使是非常大的图,也要用真实的节点和联系来表示,只不过通常会选取较小的子图来做示例。即我们更喜欢使用实例化需求来表示图
一个Cypher查询使用断言将模式的一个或多个部分锚定到图的具体位置上,然后通过缩小没有被锚定的范围来寻找附近的匹配
- 实际的图中的锚点和模式中的哪一部分绑定,是Cypher根据查询中的标签和属性断言决定的
- Cypher使用已有的索引、限制和断言中的元信息来自动计算出结果
- 还有少数情况,用它去帮助确定一些附加的线索
Cypher也是由子句组成的,最简单的查询包括:
- 一个MATCH子句
- 一个RETURN子句
- 如下所示:
MATCH (a:Person {name:[Jim]})-[:KNOWS]->(b)-{:KNOWS}->{c}
RETURN b, c
3.2 MATCH
理论上来讲,如下模式会在图数据中出现多次:
(a)-[:KNOWS]->(b)-[:KNOWS]->(c)
如果用户数据集比较大的话,可能会有很多相互的关系能够匹配这个模式。
要想锚定这个查询,我们需将它的一些部分先在图上固定。
同样,还可以用WHERE子句里的断言来表示锚定:
MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c), (a)-[:KNOWS]->(c)
WHERE a.name='Jim'
RETURN b, c
3.3 RETURN
RETURN子句用来指明已经匹配查询的数据中,哪些节点、联系和属性是需要返回给客户端的。
3.4 其他Cypher子句
- WHERE : 提供过滤模式匹配结果的条件
- CREATE和CREATE UNIQUE:用来创建节点和联系
- MERGE:保证给出的模式在图中一定存在
- 要么复用已经存在的与断言匹配的节点和联系
- 要么创建新的节点和联系
- DELETE:删除节点、联系以及属性
- SET:设置属性值
- FOREACH:对一个列表中的每个元素执行更新操作
- UNION:合并两个或更多查询的结果
- WITH:链式查询,前一个查询的结果作为后一个查询的条件。和Unix的管道命令很相似
- START:在图中制定一个或多个起始点,可以是节点、也可以是联系。(已经不推荐使用START了,更推荐在MATCH子句中指定锚点)
4 关系建模和图建模对比
一个简化的数据中心应用部署如下所示:
作为这类系统的运维方,我们主要关心两件事情:
- 为了达到(或超越)服务水平协议,目前提供的功能用来确定单点故障的前瞻性分析,以及与可用性服务相关的,用于快速定位客户投诉原因的回顾性分析。
- 为消耗的资源计费,其中包括硬件成本、虚拟化、网络供应以及软件开发和运营成本(因为这些都是我们可见的简单的系统逻辑扩展)。
当构建一个数据中心管理方案,我们希望用来存储和查询数据底层数据模型,能够保证有效地解决以上两个关心的问题。随着应用组合的变化、数据中心物理布局的发展、或是虚拟机实例的迁移,我们希望底层的模型也能随之更新。有了这些需求和限制,在看看关系建模和图建模有什么区别。
4.1 系统管理领域中的关系建模
关系世界建模寻求对这一领域实体的理解,它们是如何相关联的,以及它们状态变化的规律。大多是非正式的(可能是草稿、讨论),其示意图如下:
E-R图也是一种图,尽管它可以像图数据库一样给联系命名,但在两个实体之间,E-R图只允许建立一条无向的命名的联系。而真实世界的实体之间的联系丰富多彩,种类繁多,关系模型无法满足。
找到合适的逻辑模型之后,将它映射成表和关系,这种规范化过程可以消除数据的冗余。很多情况下,就类似于将E-R图用表格形式撰写一遍,然后用SQL命令把这些表格加载到数据库中。这时一大批出乎预料的复杂度悄悄靠近了设计的模型,如:
- 一对多联系的外键约束(标记为[FK])
- 支持多对多联系的连接表(AppDatabase)
- 当然模型中没有重复的数据
关系范型带来的挑战之一是这些规范化的模型通常都应付不了真实世界中需求变化的速度。
理论上规范化的模式应该能够回答各种突发奇想的与领域有关的问题,规范化的模式还必须为具体的访问模式做专门的特殊化处理;
-
为了让关系型数据库在处理常规应用请求时表现良好,需要接受修改用户数据模型是为了适应数据库引擎而不是用户这个现实,即反规范化(denormalization)
- 为了获得查询性能,反规范化在某些情况下,会人为制造重复数据
- 为了获得最佳效果,会把一个标准模型转换成一个反规范化模型,使其迎合底层RDBMS以及物理存储层的特性,这个过程会有大量的数据冗余产生
- “设计--> 规范化 --> 反规范化”看似可以接受,因为这是一次性的工作。但建造一个高性能的关系模型所花费的精力相对于开发整个项目的工作来说只是较小的一部分,很多时候系统变更不只发生在开发过程中,有时候产品上线后,需求还会变更
- 很多人假设系统上线后会使用很长时间,且这段时间内,生产环境都是稳定的。这个假设前半段是对的,但问题在于他们很少有稳定的时候。随着业务需求的改变和政策法规的逐步发展,系统和底层的数据结构必然也要随之改变
- 项目的设计和开发阶段,数据模型总会经历翻天覆地的变化,几乎所有的变化都是为了使模型适应应用程序的需求以便上线后可以使用,一旦上线,应用程序或是数据模型再想增加一些违背最初设计
将结构变化引入到数据库的技术机制叫做迁移(migration)。迁移提供了一种结构化的、步进式的方式对数据库进行重构,这样一来数据库可以有效的响应应用程序的变化。
- 代码重构只需几秒钟或几分钟就可以完成,但数据库重构不但耗时,并且风险高、成本高
- 反规范化模型的问题是它阻碍了系统业务需求的快速发展,为了适应关系模型,对关系模型强加了很多变化,使得概念模型和数据真实的物理布局之间产生了鸿沟
- 从业务相关方的角度来看,因为这些强加的变化,使得业务相关方无法再参与到系统进一步的发展中
- 从开发的角度,困难来源于业务需求的变化转换为底层的稳固的关系结构,使得系统的演化远远落后于业务的发展
- 没有专家的帮助和谨慎的规划,迁移一个反规范化的数据库存在很多风险
- 就是迁移完成后,如果没能和存储保持良好的关系,性能就会出问题
- 一些曾经故意制造的冗余数据成了没人要的“孤儿”,这又带来数据完整性的风险
4.2 系统管理领域中的图建模
图建模的目的:
- 可以和领域保持高度一致,且不用牺牲性能
- 支撑业务发展的同时,可以在快速的变化和增长之中保持数据的完整性的模型
图建模的工作方法:
- 第一步:与关系建模一样,用低保真的方法来描述领域并统一意见,比如用白板上的草图;
- 第二步:关系建模是将图转换成表格,图建模目的是生成一个和应用目标相关的领域部分的精确呈现。及领域中的每个实体,
- 确保将其相关角色转换成标签
- 特性转换成属性
- 与邻近实体之间的关系转换为联系
数据中心的例子,最后得出的图模型如下:
从逻辑上来说,不需要表,也不需要规范化和反规范化。一旦得到了领域模型的精确表示,把他放到数据库里简直小菜一碟。
4.3 测试模型
测试模型是为了验证领域模型是否能适应真实的查询。
- 一些设计决策像烙印一样烙进我们的程序里,阻碍我们进一步的发展
- 需要事先检查领域模型和建立的图模型,这样图结构的变化就谷安考业务变化驱动,而不用为了糟糕的设计做数据迁移
验证的方式:
- 根据图的节点沿着路径看读的句子是否逻辑正确,如“应用实例1使用了数据库实例123,分布在服务器a,b上”
- 可查询的设计:验证图是可以支持哪些我们想要的查询,必须要描述出这些查询。举例:某个用户使用某个云服务的时候出现问题,就需要搜索出用户和这个程序之间的路径,以及该程序运行所依赖的资源。
5 跨域模型
商业洞察力往往依赖于我们对复杂的价值链背后的网络效应的理解。为了达到一定程度的理解,需要联合多个领域,又不能让每个领域的细节失真或者牺牲掉。
- 属性图可以给一个价值链建模,使其成为一个图的集合,
- 每张图都有具体的联系关联其子域,又能将它们区别开来
5.1 创建莎士比亚图
- 短线表示:文学领域
- 实线表示:喜剧领域
- 长虚线表示:地理信息领域
创建脚本如下所示:
CREATE (shakespare:Author {firstname:'William', lastname:'Shakespeare'}),
(juliusCaesar:Play {title:'Julius Caesar'}),
(shakespare)-[:WROTE_PLAY {year:'1599'}]->(juliusCaesar),
(theTempest:Play {title: 'The Tempest'}),
(shakespare)-[:WROTE_PLAY {year:'1610'}]->(theTempest),
(rsc:Company {name:'RSc'}),
(production1:Production {name:'Julius Caesar'}),
(rsc)-[:PRODUCED]->(production1),
(production1)-[:PRODUCTION_OF]->(juliusCaesar),
(performance1:Performance {date:'20120729'}),
(performance1)-[:PERFORMANCE_OF]->(production1),
(production2:Production {name:'The Tempest'}),
(rsc)-[:PRODUCED]->(production2),
(production2)-[:PRODUCTION_OF]->(theTempest),
(performance2:Performance {date:'20061121'}),
(performance2)-[:PERFORMANCE_OF]->(production2),
(performance3:Performance {date:'20120730'}),
(performance3)-[:PERFORMANCE_OF]->(production1),
(billy:User {name:'Billy'}),
(review:Review {rating:5, review:'This was awsome!'}),
(billy)-[:WROTE_REVIEW]->(review),
(review)-[:RATED]->(performance1),
(theatreRoyal:Venue {name:'Theatre Royal'}),
(performance1)-[:VENUE]->(theatreRoyal),
(performance2)-[:VENUE]->(theatreRoyal),
(performance3)-[:VENUE]->(theatreRoyal),
(greyStreet:Street {name:'Grey Street'}),
(theatreRoyal)-[:Street]->(greyStreet),
(newcastle:City {name:'Newcastle'}),
(greyStreet)-[:CITY]->(newcastle),
(tyneAndWear:County {name:'Tyne and Wear'}),
(newcastle)-[:COUNTY]->(tyneAndWear),
(england:Country {name:'England'}),
(tyeAndWear)-[:COUNTRY]->(england),
(stratford:City {name:'Stratford upon Avon'}),
(stratford)-[:COUNTRY]->(england),
(rsc)-[:BASED_IN]->(stratford),
(shakespeare)-[:BORN_IN]->(stratford)
上面的语句做了两件事:
- 创建带有标签的节点(以及它们的属性)
- 将节点用联系连接起来(在需要的地方使用联系属性)
标识符(shakespeare)帮助我们将联系和这个基础节点相连。标识符在当前的查询范围内是可用的,但是出了这个范围就不行了。如果想节点或是联系一个可以长久使用的名字,需要为特定的标签或者属性组合创建索引。
5.2 开始查询
通常从一个或多个熟悉的起始点开始查询,也就是所谓的“绑定”节点。
- Cypher使用MATCH和WHERE子句里任意的标签和属性谓词
- 结合索引和约束提供的元数据
一起来寻找锚定这个图模式 的开始点
举例:
-
假设要找所有纽卡斯尔的皇家剧院演出过莎士比亚戏剧:
作者(Author):莎士比亚
地点(Venue):皇家剧院
城市(City):纽卡斯尔
-
如下:
MATCH (theater:Venue {name:'Theatre Royal'}), (newcastle:City {name:'Newcastle'}), (bard:Author {lastname:'Shakespeare'})
5.3 声明查找的信息模式
例子:找到所有在纽卡斯尔的皇家剧院演出的莎士比亚戏剧:
MATCH (theater:Venue {name:'Theatre Royal'}),
(newcastle:City {name:'Newcastle'}),
(bard:Author {lastname:'Shakespeare'}),
(newcastle)<-[:Street|CITY*1..2]-(theater)
<-[:VENUE]-()-[:PERFORMANCE_OF]->()
-[:PRODUCTION_OF]->(play)<-[:WROTE_PLAY]-(bard)
RETURN DISTINCT play.title AS play
上述MATCH模式用了几个特殊的语法元素:
- 通过指定的节点和属性值,标识符newcastle、theater和bard都被锚定到图中真实的节点
- 数据库中有多个皇家剧院,比如:普利茅斯、巴斯、温彻斯特和诺里奇这些城市都有一个皇家剧院,那么theater会绑定到所有的这些节点上。为了进一步找到在纽卡斯尔的皇家剧院,增加了<-[:Street|CITY*1..2]-,这句的意思是剧院的节点和纽卡斯尔的节点之间不应该超过两条以上的Street或CITY的联系。通过提供一个可变的路径长度,允许细粒度的地址分出层次(例如由街、区或自治区和市构成的地址)
- (theater)<-[:VENUE]-()使用了匿名节点,即空括号。因为了解数据,所以知道匿名节点将会匹配到一些演出,但是对每场演出的具体细节并不关心。在查询的其他地方也不会用到,所以就没有把他绑定到任何标识符上。
- 在将演出连接到作品时,又一次用到了匿名节点()-[:PERFORMANCE_OF]->()。如果对这些演出或是原著有兴趣,也可以使用标识符来替代匿名节点
- MATCH的剩余部分就是一个简单的“节点到联系到节点”的模式:(play)<-[:WROTE_PLAY]-[bard]。保证查询只返回莎士比亚所写的戏剧。因为(play)连接到匿名的作品节点,然后通过连接到的演出节点,可以有把握的推断这部喜剧应该在纽卡斯尔的皇家剧院演出过。把这个戏剧节点命名后,就可以在后面的查询里用到这个标识符
5.4 约束匹配
WHERE子句可以限制图查询,基于以下规则:
- 匹配的子图中必须有(或者没有)限制的路径
- 节点必须有指定的标签或者指定名字的联系
- 在匹配的节点或联系上的某个属性必须有(或者没有)特定的属性,无论它们的值是什么
- 在匹配的节点或联系上的某个属性必须有特定的属性值
- 必须能满足其他任意复杂的表达式断言(如:在某个特定的日期或者之前必须有演出上演)
例子:如果需要将结果的范围缩小到莎士比亚晚期的戏剧,通常是指1608年前后,这个可以通过过滤WROTE_PLAY联系上的year属性来达到目的。调整语句如下所示:
MATCH (theater:Venue {name:'Theatre Royal'}),
(newcastle:City {name:'Newcastle'}),
(bard:Author {lastname:'Shakespeare'}),
(newcastle)<-[:Street|CITY*1..2]-(theater)
<-[:VENUE]-()-[:PERFORMANCE_OF]->()
-[:PRODUCTION_OF]->(play)<-[w:WROTE_PLAY]-(bard)
WHERE toInt(w.year) > 1608
RETURN DISTINCT play.title AS play
上面的语句还用到了如下语法:
- toInt(w.year) > 1608 将字符串转换车数值,再进行比较
- 当然也可以 w.year > '1608' 进行比较,但是数字的字符串最好转换为数值进行比较,所以如果字符串本身就是数值,最好以数值的形式存储在数据库中
举例:前面将演出的year字段设置成了字符串,现在需要将其改成年份
MATCH (bard:Author {lastname:'Shakespeare'}),
p = (play)<-[w:WROTE_PLAY]-(bard)
SET w.year = toInt(w.year)
RETURN p
5.5 处理结果
RETURN DISTINCT play.title AS play:其中DISTINCT 保证返回的结果是唯一的
RETURN count(play):统计演出的次数
-
如果根据演出次数对结果排序,可以对PERFORMANCE_OF绑定一个标识p,然后对这个p进行计数(count)和排序,如下:
MATCH (theater:Venue {name:'Theatre Royal'}), (newcastle:City {name:'Newcastle'}), (bard:Author {lastname:'Shakespeare'}), (newcastle)<-[:Street|CITY*1..2]-(theater) <-[:VENUE]-()-[p:PERFORMANCE_OF]->() -[:PRODUCTION_OF]->(play)<-[:WROTE_PLAY]-(bard) RETURN play.title AS play, count(p) AS performance_count ORDER BY performance_count DESC
5.6 查询链
有时候一个MATCH得到一切是不可能的。WITH子句允许将几个匹配连接到一起,将前一个查询的结果当做条件输送到下一个查询中。
举个例子:查询一些莎士比亚的戏剧,然后按照写作的年份将它们排序,年代最近的排在最前面。使用WITH子句,将结果输送到RETURN子句里,它使用collect函数输出一个用逗号分隔的戏剧名称列表
MATCH (bard:Author {lastname:'Shakespeare'}) -[w:WROTE_PLAY]->(play)
WITH play
ORDER BY w.year ASC
RETURN collect(play.title) AS plays
上面的语句中需要先对play进行排序,然后再对play进行collect处理。
WITH可以用来将只读子句从以写入为中心的SET操作中分离出来。
WITH通过可以把复杂的查询分解成多个简单模式,将复杂的查询分而治之。
6 建模时常见的陷阱
虽然图建模是掌握复杂的问题域的一种极具表现力的方式,但只有表现力并不能保证每一个图都适合其用途。下面说一些常见的有问题的模型
6.1 电子邮件起源问题域
信息交流模式分析是一个经典的图问题,涉及用途去发现领域专家、关键影响力以及信息传播的通信通道。但在这个场景下,我们寻找的是一个坏蛋,而不是正面的榜样或是专家:就是可疑的电子邮箱通信模式,很可能是违反公司规定,甚至是违法。
6.2 敏感的第一个迭代
早期的模型,如下所示:
CREATE (alice:User {username:'Alice'}),
(bob:User {username:'Bob'}),
(charlie:User {username:'Charlie'}),
(davina:User {username:'Davina'}),
(edward:User {username:'Edward'}),
(alice)-[:ALIAS_OF]->(bob)
该语句中很容易看出“Alice是Bob的一个别名”。如下图所示:
!
接下来用他们曾经相互发送过的电子邮件记录来把用户连接起来
MATCH (bob:User {username:'Bob'}),
(charlie:User {username:'Charlie'}),
(davina:User {username:'Davina'}),
(edward:User {username:'Edward'})
CREATE (bob)-[:EMAILED]->(charlie),
(bob)-[:CC]->(charlie),
(bob)-[:BCC]->(charlie),
这种描述乍一看合理且忠实于领域的表示方式。从上面的图可以看到“Bob给Charlie发了电子邮件”,但Bob到底在电子邮件中写了什么,虽然我们可以看到的是Bob抄送或是密件抄送了一些人,但看不到最重要的东西:电子邮件本身。如下图所示:
运行下面的查询时,这个图结构带来的信息缺失显得尤为明显:
MATCH (bob:User {username:'Bob'})-[e:EMAILED]->
(charlie:User {username:'Charlie'})
RETURN e
这个查询返回了Bob和Charlie之间的EMAILED联系。只让我们知道有电子邮件交流,而不能告诉我们电子邮件是什么,如下所示:
也许在EMAILED联系上加一些代表电子邮件特性的属性就可以挽救局面,但那其实只是在拖延时间,我们还是无法知道EMAILED、CC和BCC这些关系之间是如何相互作用的,也就是说不清楚哪些电子邮件是抄送的,哪些是密件抄送的,以及它们都是发给谁的。
6.3 第二次魅力
要修复这个有缺失的模型,需要加入电子邮件节点来代表在业务中来往的电子邮件,并扩展联集包含所有电子邮件支持的地址信息。替换掉这个有结构缺失的语句:
CREATE (bob)-[:EMAILED]->(charlie)
用如下语句创建包含更多细节的结构:
CREATE (email_1:Email{id:'1', content:'Hi Charlie,.... Kind regards, Bob'}),
(bob)-[:SENT]->(email_1),
(email_1)-[:TO]->(charlie),
(email_1)-[:TO]->(davina),
(email_1)-[:CC]->(alice),
(email_1)-[:BCC]->(Edward)
6.4 发展中的领域
在图库中,更倾向于添加新的节点和联系来增加信息或者成分,而不是修改已有的模型。这样做不会影响已有的查询,并且是完全安全的
使用已有的联系类型对图做修改,或者修改现有节点的属性(不仅仅是属性值)有可能是安全的,但是我们需要运行一些有代表性的查询来增强我们的信心,来告诉我们即时图的结构改变了,仍然可用
7 辨别节点和联系
建模的过程可以非常合适地总结为用图结构来表达想问我们的领域的问题。也就是说,我们所说的面向可查询的设计:
- 描述驱动模型的客户端或者最终用户的目标
- 把这些目标转述成要问的领域问题
- 明确这些问题中出现实体和联系
- 把这些联系和实体翻译成Cypher的路径表达方式
- 用图的模式来表达我们想问的领域问题,使用路径表达式,就如同我们建模这个领域时一样
通过拷问用来描述领域的语言,可以快速明确图中的核心元素:
- 常用的名字可以作为标签,如:user、email,变成标签User和Email
- 带有宾语的动词可以作为联系名称,如:sent、wrote,变成SENT和WROTE
- 一个合适的名词(比如人们或者公司名)指代一样东西的实体,就把他建模为节点,用一个或是多个属性来记录他的特点
8 避免反模式
- 不要把实体建模成联系,应用使用联系来传达实体之间如何相连的,以及这些联系的性质;
- 动词名词话(一个名词可以借此转型为动词的语言习惯)经常影藏了名词的出现以及相应的领域实体;
- 理解图的天然可扩展性,增加一些领域实体以及增加在它们之间使用新的节点和新的联系进行连接的方式,这些看起来像是往数据里猛灌大量的数据,但其实是很自然的事情
- 在写入数据时,为了保证查询效率而混入数据元素是很糟糕的做法。
- 按照向领域提出的问题来建模,就能得到一个精确表示领域的模型。确立这样的数据模型,图数据库在读的时候就能表现的很好。
- 即使存储了非常大量的数据,图数据库的查询速度依然良好。
当我们学着去组织我们的图并且不用去反规范化它们的时候,我们要学会去相信图数据库,这是很重要的。
9 小结
- 图数据库赋予了软件开发人员用图表达问题领域的能力,并可以在运行时查询图
- 图可以清晰的描述问题域;
- 图数据库可以让我们在存储这种描述方式时得以保留领域和数据之间的亲缘关系
- 图建模免去了使用复杂的数据管理代码来规范化和反规范化数据这一步骤
- 我们创建的图应该让那些查询语句读起来很顺畅,同时要避免混杂实体和动作,在多个迭代中满足系统的需求,同时也可以跟上代码的演变;