现在我们已经了解了关系型实现,让我们研究一下如何把样本数据转换为图数据库实现。在进入实现细节的讨论之前,让我们重温如图3-12所示的概念模型。
对于这个例子,我们将使用Gremlin查询语言——最广泛应用的图查询语言——以及DataStaxGraph结构API。我们选用Gremlin因为它在图数据库社区内被广泛采用,并且致力于开源。本书的首要目标是在分布式的分区环境中实现图。鉴于这一目标,我们将使用DataStaxGraph结构API来建立分布式图。
图3-12:对表3-1中的数据间关系的概念描述
和关系型模型相比,从概念模型到图数据模型的转换更小。这种更低的成本显现了更接近于自然描述数据方式的数据库实现的力量。
使用2.5节的图结构语言,图3-13包含了样本数据的属性图模型。首先要注意的好处是,图实现从概念(图3-12)到逻辑数据建模的过渡更小。
图3-13:基于图实现的C360应用程序数据模型
图3-13中有4个顶点标签:客户(Customer)、账户(Account)、信用卡(CreditCard)和贷款(Loan)。在图模型中,这些顶点标签用粗体在实体上表示。图3-13中有3个边标签:拥有(owns)、使用(uses)和借有(owes)。
最后,在图3-13中我们还可以看到一些地方使用了属性。一个客户顶点有两个属性:customer_id和name。你可以看到每个顶点的属性列在顶点标签下面。而且我们还在owns的边标签上涵盖了一个role。
图数据库实现的第一步是创建图,以便于添加图结构。一旦我们创建好结构,就可以往数据库里插入数据了。
创建图的代码如下:
提供的技术资料中已经处理了图的安装和配置。如果你想深入了解这些步骤,那么可以在DataStax文档( https://oreil.ly/_i_m7 )中找到分步说明。本书将不赘述这些主题。
让我们直接来创建图结构。如果你愿意,那么可以在我们为本章创建的DataStax Studio笔记本Ch3_SimpleC360中进行学习。DataStax Studio( https://oreil.ly/Zt_JY )为你提供了一个使用DataStax产品进行开发的笔记本环境,是实现本书例子的最佳途径。笔记本可以在本书的GitHub仓库( https://oreil.ly/graph-book )中找到。
创建图结构
首先我们来创建Customer顶点标签。我们的客户数据有唯一ID和一个名字:
随后我们为账户、贷款和信用卡添加顶点标签以完成顶点标签的创建:
到这里,数据库里有四张表了——每个顶点标签一张表。最后一步是添加数据模型中客户到其他实体之间的关系。
在这个例子中,我们选择了从客户顶点出来并进入其他顶点类型的边的模型。这些边是有方向的;它来自客户并进入账户、贷款和信用卡。当我们创建边标签时,这个方向很重要。让我们看一个例子,在一个客户和他们的账户之间创建借有(owes)关系。
这条边标签的方向使用from和to两步来设置。这条边从顶点标签Customer出发指向顶点标签Loan。
还有两个边标签需要创建:一条从客户到他们的信用卡,另外一条是从客户到他们的账户。边owns也会在上面存一个role属性:
我们阅读标签的时候会说owns边从(from)客户出发到(to)账户,并且有一个叫作角色(role)的属性。
插入图数据
有了图结构,我们就可以把样本数据添加到图数据库中。从一条数据开始——Michael的顶点:
在向图中添加顶点时,addV步骤要求你提供完整的主键。否则,你会看到一个类似图3-14所示的错误。
图3-14:你可能会遇到的错误示例,如果你在插入新的顶点时忘记包含完整的主键
接下来我们来添加Michael的账户、贷款和信用卡的顶点:
步骤next()在Gremlin中是一个结束语句。它从遍历的终点返回第一个结果。在前面的例子中,我们返回刚刚添加到图中的顶点对象,并将其存储在内存变量中。
现在,我们的图数据库中有四块互不相连的数据。和以前一样,我们将每个顶点对象存储在名为acct_14、loan_32和cc_17的变量中,以便后续使用。实际上该数据库有四个顶点,没有边,如图3-15所示。
让我们引入一些数据之间的关联。要做到这一点,需要从customer_0到其他顶点添加三条边。使用刚刚创建的变量,可以从顶点Michael到顶点Account、Loan和CreditCard分别添加一条边:
图3-15:当前图数据库中的数据
在往数据库中添加边时,首先要确定它来自哪个顶点。在前面的例子中,就是Michael,因为所有的边都将从Michael开始,然后去到其他的数据。这三条边在图数据库中形成了第一个连接的数据视图,如图3-16所示。
从已经看过的例子中,我们知道Maria与Michael共享一个账户。让我们为Maria添加顶点,并将其连接到已经创建的账户顶点(如图3-17所示):
图3-16:当前图数据库中数据的连接视图
图3-17:图数据库中Michael和Maria数据的连接视图
让我们添加其余三个客户的顶点和边来完成这个例子:
最后这些语句完成了将样本数据插入到图数据库的过程。图3-18展示了图数据库中数据的最终视图。
图3-18:图数据库中数据的最终视图
图遍历
本节的Gremlin语句是最初的图数据库查询。图数据库查询也叫作图遍历(graph traversal)。
图遍历
图遍历是一个以明确定义的顺序访问图的顶点和边的迭代过程。
当使用Gremlin时,你从一个遍历源(Traversal source)开始遍历。
遍历源
遍历源包含两个概念:将要遍历的图数据,以及遍历策略,比如探索没有索引的数据。在本书的例子中,你将使用的遍历源是dev(用于开发)和g(用于生产)。
本节中的查询使用了g遍历源。我们将在第5章和生产章节中再次讨论使用g遍历源。
在本章剩余部分,我们将使用dev遍历源。在本书中,在进行图遍历时,我们将始终使用dev遍历源,比如在本章、第4章和开发章节中。我们使用dev遍历源是因为它允许我们在没有数据索引的情况下探索图数据。
从这里开始,让我们继续实现与之前相同的查询,但使用的是图数据。
我们喜欢把查询图数据库大致认为是SQL查询的反面。常见的关系型查询思维是SELECT-FROM-WHERE。而在图中,遍历基本上遵循的是类似但反向的模式:WHERE-JOIN-SELECT。
你可以认为Gremlin查询从图数据中需要开始的地方(WHERE)开始。然后,你告诉数据库使用来自你的起始位置的关系,将不同的数据片段连接(JOIN)起来。最后,你要告诉数据库哪些数据要被选取(SELECT)并返回。对于C360应用程序来说,我们的查询大致遵循这种WHERE-JOIN-SELECT模式,是学习如何查询图数据库的一个很好的起点。
考虑到这一点,让我们重新审视一下C360应用程序查询,然后我们将使用Gremlin查询语言和我们的图数据库回答每个问题:
1.这位客户使用了哪些信用卡?
2.这位客户拥有哪些账户?
3.这位客户借有哪些贷款?
4.我们对这位客户有什么了解?
问题1:这位客户使用了哪些信用卡
首先,让我们用图数据库来查询customer_0拥有的信用卡。我们不能上来就查询任意信用卡,得从访问customer_0的顶点开始,然后走向邻接(连接)customer_0的信用卡。在Gremlin里:
在每行代码//右面的语句是内嵌注释,来描述其左侧代码的内部逻辑。
这个查询将返回如下数据:
让我们把这条Gremlin查询分解成WHERE-JOIN-SELECT模式。查询的第一个部分为dev.V().has("Customer","customer_id","customer_0")。我们说这一步是找到从哪里开始你的图遍历;我们从找到一个标签为Customer且customer_id等于customer_0的顶点开始。遍历的第二步是out("uses")。这步是连接客户到他们的信用卡数据。最后一步是选择你想要返回的数据。也就是values("cc_num")。这部分的Gremlin遍历是在指定选择哪些数据返回给最终用户。
每当你看到遍历这个词时,你都可以将其与行走的概念联系起来。对我们来说,图遍历就是遍历你的图数据。当我们写图遍历时,我们会在脑海中想象往返于图数据片段的情景。
让我们回到刚写的图查询,来阐述我们是如何把遍历想成是在图数据中行走的。在这个图查询的第一步,我们找到了一个顶点作为出发点:customer_0。从这位客户开始,我们需要穿过向外延伸的边标签:uses。在Gremlin里,我们用out()步骤走过这条边,这样就能到达信用卡顶点。一旦到达了信用卡顶点,就可以浏览顶点的属性了。具体来说,我们想要得到Michael的信用卡号:cc_17。
为了获得最佳性能,我们建议你总是通过完整主键找到一个具体的顶点开始你的图遍历。对于Apache Cassandra用户也是一样,你需要给CQL查询提供完整主键。
当你开始练习写头几个图遍历的时候,手头有一份图3-13的备份会方便很多。通过纸上的一幅图,你可以看到你需要从哪儿开始、到哪儿结束。这就像是使用地图导航,但是在这个场景里,你是行走在数据中。有了图数据,你可以用图模型来找到起点和终点,并将它们之间的行走转化为Gremlin语句。只要有足够的练习,你最终将能够在脑海中完成这一切。
问题2:这位客户拥有哪些账户
我们应用的下一个C360查询想要知道某位具体的客户拥有哪些账户。遵循和之前一样的模式,我们将要访问customer_0的顶点,然后走向账户顶点。从账户顶点,我们可以得到这个账户的唯一ID:
和前面一样,这个查询遵循WHERE-JOIN-SELECT模式。这个Gremlin查询的第一部分和WHERE语句类似:dev.V().has("Customer","customer_id","customer_0")。我们说这步是找到从哪里开始你的图遍历。
遍历的第二步更像一个连接语句:out("owns")。这一步是沿着从customer出发的owns关系连接其关联的数据。最后一步选择数据返回给最终用户,具体来说就是账户ID:values("acct_id")。该查询将返回如下数据:
让我们再次尝试同样的查询,但这次我们想同时显示客户的名字和他们的账户ID。要做到这一点,需要记住我们在走过图时访问过的数据。这就引入了两个新的Gremlin步骤:as()和select()。as()步骤类似于在你走过图时给数据贴上标签,就像你在迷宫中行走时留下面包屑一样。
做完这步,我们可以用另外一个新步骤来回忆访问数据:select()。我们用select()步骤来返回查询的数据:
和前面一样,这个查询遵守同样的WHERE-JOIN-SELECT模式,并用了两个查询条件。这个查询加入了SAVE和SELECT的需求,从查询中来保存和选择特定的数据点。
让我们来看一下这个查询的具体步骤。
再来一次,从在图数据中所需的哪里开始,dev.V().has("Customer","customer_id","customer_0")。我们希望为后面记住这个数据,所以我们用as("customer")保存这步的数据。然后我们继续按着前面的模式,通过owns边连接这个客户和他的账户数据。现在我们到达了账户顶点。我们希望通过as()保存这个顶点,和前面一样。最后我们需要选择多块数据。我们用select("customer","account")来做到这点。
剩下的两个使用by的步骤是很重要的,所以提出来讲一下。这个步骤帮助我们塑造查询结果。在select("customer","account")步骤之后,我们有两个顶点对象:分别是客户和账户顶点。我们最初的查询想要访问客户的名字和账户ID。这就是by步骤的作用。我们想根据客户的名字来查看客户,根据账户的ID来查看账户。by步骤是按顺序作用于顶点对象的。
这个查询返回如下JSON:
问题3:这位客户借有哪些贷款
到目前为止,我们已经看到了三种图遍历和两种从图中选择数据的方式。接下来,让我们探讨一下C360应用程序的第三个问题。这个问题想要访问客户相关的贷款。在这个例子中,我们使用customer_4,因为其在我们的数据集中有多个贷款。在这个查询中,我们只想看一下贷款的ID。
这个查询遵循和前面一样的WHERE-JOIN-SELECT模式。该查询将返回如下数据:
问题4:我们对这位客户有什么了解
对C360应用程序的终极问题是访问单个客户的所有相关数据。这个问题将从customer_0开始,遍历所有与customer_0相连的外边。然后,我们返回位于customer_0的一级邻接点的所有顶点数据。这个查询提供给我们关于customer_0的所有数据。
该查询会返回例3-1所示数据。
例3-1:
例3-1展示了存储在DataStax Graph中关于每个顶点的所有内容:一个内部id、顶点的标签(label),然后是所有属性。让我们检查一下描述Michael的信用卡的JSON。首先,有一个"id":"dseg:/CreditCard/cc_17"。这是DataStax Graph中用来描述这段数据的内部标识符。DataStax Graph的内部ID是URI,即统一资源标识符。接下来,我们看到顶点的标签,"label": "CreditCard"。最后,我们看到我们在图中存储的关于信用卡的唯一属性,"cc_num": "cc_17"。我们以类似的方式解释关于贷款和账户顶点的JSON。
这些遍历是在你的C360应用程序中提取数据所需的基础。我们建议在你刚开始编写图遍历时,在手边保留一份图数据模型的副本。一旦你理解了基本步骤,你就可以使用数据模型图,从起点走到目的地。就像是一门艺术,经过一些练习,你可能会在脑海中把它可视化,你仿佛就是那个在数据中行走的人。
我们构建这个例子是为了说明图的应用可以使数据检索更容易。正如本节所见,查询的步骤明显减少,而且更容易操作。从关系型查询语言到图查询语言,需要调整你对数据的遍历或行走的思维方式。学习曲线是很陡峭的,我们不想隐藏这一点。然而,一旦你能想象自己在图数据中行走,编写图查询就会像学习一套新的工具一样简单。