03 使用图进行数据建模

2019-10-17  本文已影响0人  武漂的小丙

1 模型和目标

建模是为了让不规则的领域的一些具体方面变成结构化的、可操纵的空间。对于事物实际存在的方式,并没有一种天然的表达方式,我们只能有目的地选择、抽象和简化,一些方法能更好的满足某个特定目标。

图建模与其他建模技术的不同之处在于其逻辑模型和物理模型之间有更加密切的关系。关系型数据管理技术背离了用自然的语言来描述领域:

2 带标签的属性图模型

带标签的属性图模型主要有以下几个特征:

3 查询图:Cypher简介

3.1 Cypher的理念

1570502605160.png

这个模式描述了3个有交集的朋友,用ASCII字符画表达出来就是:

(emil)<-[:KNOWS]-(jim)-[:KNOWS]->(ian)-[:KNOWS]->(emil)

这个模式描述了一条路径,它将一个叫jim的节点和两外两个叫ian和emil的节点连接起来,同时也将ian节点和emil节点连接起来。

要注意:ian、jim和emil是标识符。标识符可以让我们在描述一个模式时,多次指向同一个节点(可以帮我们绕过查询语句其实只有一个方向的事实【只能从左到右处理文本】),而示意图可以从两个防线展开。除了偶尔需要使用这种方式重复使用标识符,整个语句的意图仍然是清晰的。

示意图一般倾向于使用特定节点或联系的实例,而不是类或原型。即使是非常大的图,也要用真实的节点和联系来表示,只不过通常会选取较小的子图来做示例。即我们更喜欢使用实例化需求来表示图

一个Cypher查询使用断言将模式的一个或多个部分锚定到图的具体位置上,然后通过缩小没有被锚定的范围来寻找附近的匹配

Cypher也是由子句组成的,最简单的查询包括:

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子句

4 关系建模和图建模对比

一个简化的数据中心应用部署如下所示:

1570581978511.png

作为这类系统的运维方,我们主要关心两件事情:

当构建一个数据中心管理方案,我们希望用来存储和查询数据底层数据模型,能够保证有效地解决以上两个关心的问题。随着应用组合的变化、数据中心物理布局的发展、或是虚拟机实例的迁移,我们希望底层的模型也能随之更新。有了这些需求和限制,在看看关系建模和图建模有什么区别。

4.1 系统管理领域中的关系建模

关系世界建模寻求对这一领域实体的理解,它们是如何相关联的,以及它们状态变化的规律。大多是非正式的(可能是草稿、讨论),其示意图如下:

1570693477513.png

E-R图也是一种图,尽管它可以像图数据库一样给联系命名,但在两个实体之间,E-R图只允许建立一条无向的命名的联系。而真实世界的实体之间的联系丰富多彩,种类繁多,关系模型无法满足。

找到合适的逻辑模型之后,将它映射成表和关系,这种规范化过程可以消除数据的冗余。很多情况下,就类似于将E-R图用表格形式撰写一遍,然后用SQL命令把这些表格加载到数据库中。这时一大批出乎预料的复杂度悄悄靠近了设计的模型,如:

1570694322351.png

关系范型带来的挑战之一是这些规范化的模型通常都应付不了真实世界中需求变化的速度。

4.2 系统管理领域中的图建模

图建模的目的:

图建模的工作方法:

数据中心的例子,最后得出的图模型如下:

1570786586881.png

从逻辑上来说,不需要表,也不需要规范化和反规范化。一旦得到了领域模型的精确表示,把他放到数据库里简直小菜一碟。

4.3 测试模型

测试模型是为了验证领域模型是否能适应真实的查询。

验证的方式:

5 跨域模型

商业洞察力往往依赖于我们对复杂的价值链背后的网络效应的理解。为了达到一定程度的理解,需要联合多个领域,又不能让每个领域的细节失真或者牺牲掉。

5.1 创建莎士比亚图

1571122593315.png

创建脚本如下所示:

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 开始查询

通常从一个或多个熟悉的起始点开始查询,也就是所谓的“绑定”节点。

一起来寻找锚定这个图模式 的开始点

举例:

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模式用了几个特殊的语法元素:

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

上面的语句还用到了如下语法:

举例:前面将演出的year字段设置成了字符串,现在需要将其改成年份

MATCH (bard:Author {lastname:'Shakespeare'}),
      p =  (play)<-[w:WROTE_PLAY]-(bard)
SET w.year = toInt(w.year)
RETURN p          

5.5 处理结果

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的一个别名”。如下图所示:

1571310979866.png

!

接下来用他们曾经相互发送过的电子邮件记录来把用户连接起来

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抄送或是密件抄送了一些人,但看不到最重要的东西:电子邮件本身。如下图所示:

1571318949732.png

运行下面的查询时,这个图结构带来的信息缺失显得尤为明显:

MATCH (bob:User {username:'Bob'})-[e:EMAILED]->
       (charlie:User {username:'Charlie'})
RETURN e

这个查询返回了Bob和Charlie之间的EMAILED联系。只让我们知道有电子邮件交流,而不能告诉我们电子邮件是什么,如下所示:

1571319365142.png

也许在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 辨别节点和联系

建模的过程可以非常合适地总结为用图结构来表达想问我们的领域的问题。也就是说,我们所说的面向可查询的设计:

  1. 描述驱动模型的客户端或者最终用户的目标
  2. 把这些目标转述成要问的领域问题
  3. 明确这些问题中出现实体和联系
  4. 把这些联系和实体翻译成Cypher的路径表达方式
  5. 用图的模式来表达我们想问的领域问题,使用路径表达式,就如同我们建模这个领域时一样

通过拷问用来描述领域的语言,可以快速明确图中的核心元素:

8 避免反模式

当我们学着去组织我们的图并且不用去反规范化它们的时候,我们要学会去相信图数据库,这是很重要的。

9 小结

  1. 图数据库赋予了软件开发人员用图表达问题领域的能力,并可以在运行时查询图
    • 图可以清晰的描述问题域;
    • 图数据库可以让我们在存储这种描述方式时得以保留领域和数据之间的亲缘关系
    • 图建模免去了使用复杂的数据管理代码来规范化和反规范化数据这一步骤
  2. 我们创建的图应该让那些查询语句读起来很顺畅,同时要避免混杂实体和动作,在多个迭代中满足系统的需求,同时也可以跟上代码的演变;
上一篇 下一篇

猜你喜欢

热点阅读