1. 使用熟路构建遍历
1.1. 为了避免把开发遍历和构建应用程序混淆
-
1.1.1. 把遍历的编写和测试与应用程序的开发拆分成了两个独立的操作
-
1.1.2. 在Java代码之外独立开发遍历,然后把它们加入应用程序中
-
1.1.3. 现实情况是,大多数开发人员会同时完成两者,不管使用的是哪种数据库引擎
1.2. 开发熟路遍历要识别模式的有关部分,寻找遍历的起点和终点,识别从起点到终点遍历所需的一系列顶点和边,最后循序渐进、迭代式地增加操作来组成遍历,并通过测试数据验证每一步的结果
1.3. 好的遍历起点可以最小化开始顶点,最理想的情况是只有一个顶点
2. 开发遍历的准备工作
2.1. 用例的需求
- 2.1.1. 为用例开发遍历要从识别回答业务疑问所需的顶点和边开始
2.2. 图数据模型
2.3. 在把业务疑问转换为技术需求与实现时,代词很容易被忽略,这会隐藏额外的、更细微的需求
2.4. 识别所需的元素
-
2.4.1. 检视每一个需求,并将其拆分成回答该疑问所需的组件
-
2.4.2. 识别所需的顶点标签
-
2.4.2.1. 最好的起点就是寻找需求中的名词
-
2.4.2.2. 在数据模型中寻找这些名词对应的顶点标签
-
2.4.2.3. 定位起点是为了寻找和疑问有关的部分,从而把遍历收缩到数量最少的开始顶点
-
2.4.3. 识别所需的边标签
-
2.4.3.1. 一旦识别了需求中的动词,就可以查找数据模型中相应的边名了,就像找名词那样
2.5. 选择起点
-
2.5.1. 选择我们认为最具挑战性的问题,并从它入手
-
2.5.1.1. 这个方式适合有大量未知因素或大量项目风险的情况
> 2.5.1.1.1. 向开发生态系统引入新技术或新流程
- 2.5.1.2. 这个方式能让我们快速失败,也是需要快速决策时的正确选择
> 2.5.1.2.1. 需要决定是否继续> 2.5.1.2.2. 确定某项技术是否是解决某个问题的正确选择
-
2.5.2. 从最直接或最不复杂的疑问开始,并以此作为之后工作的基石
-
2.5.2.1. 这条道路倡导代码的渐进式开发,也很好地避免了囫囵吞枣
-
2.5.2.2. 该方式的意义在于快速获取成功,或者在处理更复杂的问题之前,从更小、更简单的问题中获得成就感
-
2.5.2.3. 从最不复杂的疑问开始,我们就能在解决更复杂的问题之前从更小、更简单的问题那里获得成功
2.6. 准备测试数据
-
2.6.1. 加载测试数据
-
2.6.2. 在对数据有错误假设的其他情景中,增加样本数据可能是最佳方式
-
2.6.3. 在排查此类错误的时候,你手上还有其他资源
-
2.6.3.1. 和同事讨论问题能帮助你获得解决方案
-
2.6.3.2. 另一种可行的方式是尝试以更受控的方式复制相关条件,也许是使用更小的数据集
-
2.6.4. 采用一步接一步的系统性方式构建遍历可以使遇到错误时的排查变得更容易
3. 编写第一个遍历
3.1. 设计遍历
- 3.1.1. 布丁好不好,吃过才知道
3.2. 开发遍历代码
-
3.2.1. 在Gremlin Server里,日期是以世界协调时(Coordinated Universal Time,UTC)的格式保存的
-
3.2.1.1. 为了显示,Gremlin Console会自动将其转换成本地时区的日期
-
3.2.2. 通过ID扩展遍历
-
3.2.3. 开发熟路遍历所需的功夫确实比开发社交网络需要的多,但最艰巨的任务已经完成了
-
3.2.4. mean():聚合一组值来计算平均值,常用与group().by().by()操作搭配使用
-
3.2.5. has()操作是首要的筛选操作,也是基于属性实现筛选逻辑的首选
-
3.2.6. where()操作通常用于其他情况的筛选——基于比简单属性匹配更复杂的逻辑组合的筛选
-
3.2.7. identity():获取进入该操作的元素并原封不动地返回
-
3.2.8. optional(traversal):尝试遍历,如果返回结果,则发出结果;否则,发出传入元素(与identity()操作一样)
-
3.2.9. lues标识和values()操作是不一样的
-
3.2.9.1. values标识指向键-值对的值部分
-
3.2.9.2. values()指定从元素中返回的属性
3.3. 对遍历进行分组和排序会创建键-值对形式的结果
- 3.3.1. 对键-值对的进一步处理会采用select()操作的一个特殊重载,分别对键-值对中键的部分和值的部分进行处理
3.4. 在遍历过程中越早进行筛选,意味着在图中移动的遍历器越少,因此要做的总体工作也越少
4. 分页和图数据库
4.1. offset(偏移值)
-
4.1.1. 要跳过的记录数量
-
4.1.2. 在数据集的起点,offset =0
-
4.1.3. 偏移值是分页大小的倍数
-
4.1.3.1. 如果分页大小是10,偏移值则可能是0、10、20、30等
4.2. limit(限制值)
-
4.2.1. 分页大小或要返回项的最大数量
-
4.2.2. 限制值是要返回项的最大数量,是因为结果集并不总是分页大小
-
4.2.2.1. 最后一页所含的项比分页大小要少
4.3. range(startNumber, endNumber)
-
4.3.1. 穿过各个对象,从startNumber开始(包含该索引位置的对象),直到endNumber(不包含该索引位置的对象)
-
4.3.2. startNumber是包含在返回结果中的
-
4.3.2.1. startNumber和offset一样
-
4.3.3. endNumber被排除在返回结果以外
-
4.3.3.1. endNumber则不是关系数据库中使用的limit,而是startNumber +limit
-
4.3.3.2. 分页功能要以offset和limit作为常规输入,计算endNumber
-
4.3.4. 调用range()前为输入排序的重要性
-
4.3.4.1. 数据库引擎基于其内部逻辑决定结果的顺序
-
4.3.4.2. 要给用户提供一致的体验,就必须在调用range()操作前进行排序
-
4.3.5. 排序是昂贵的操作
-
4.3.5.1. 对结果进行排序在任何数据库中都是昂贵的操作,特别是对于大型数据集来说
-
4.3.5.2. 键-值对很容易使用,前提是能以它们的值来排序
4.4. 在关系数据库中处理分页的通用模式和在图数据库里是一样的
-
4.4.1. 获取遍历的结果
-
4.4.2. 找到offset索引指定的记录
-
4.4.3. 返回结果的limit数
-
4.4.4. 根据新的偏移值offset + limit重复以上过程
4.5. 方式1是一直运行遍历,递增offset,直到它返回空的结果集为止
- 4.5.1. 适应的场景是:预期的结果数量很大,而且不需要知道总数;或者想避免提前为所有结果计数的开销
4.6. 方式2是预设可能的结果总数,并把它作为offset + limit值的上限
- 4.6.1. 对于应用程序需要知道显示结果总数的场景特别有用
4.7. 在图遍历中对结果进行分页需要以排序后的结果集作为输入,并运用限制值和偏移值指定期望的结果子集
5. 五个通用操作
5.1. general step
5.2. map、flatMap、filter、branch、sideEffect
- 5.2.1. 这五个通用操作是编程的核心概念
5.3. 所有的Gremlin操作,除了调整或配置其他操作的操作之外,本质上都是这五个通用操作之一的优化版本
6. 子图
6.1. 个性化是基于数据中的连接关系筛选数据以提供最相关内容的过程
-
6.1.1. 只专注于数据的一个子集,而忽略另一个子集
-
6.1.2. 子图天然适合个性化问题
6.2. 因为只就图中明确定义的部分数据提出疑问,所以我们只想处理这个数据子集
-
6.2.1. 做到这一点的最有效方法是从全局图中提取该数据子集
-
6.2.2. 这是一个常见的操作,该数据子集称为子图
-
6.2.3. 就是顶点和边的子集,通常根据某种规则或对业务领域的理解而紧密相连
6.3. 子图是图数据的子集,包含用来表示图的顶点和边
-
6.3.1. 子图本身就是图
-
6.3.2. 意味着我们可以在子图上运行遍历,但是因为子图被限制在一小部分顶点和边上,所以只需要更少的内存和计算能力
7. 使用子图
7.1. 并不是所有的图数据库产品都有显式的子图支持
7.2. 子图也是图,只是其中的所有顶点和边都是一个更大的图的子集
-
7.2.1. 子图本身就是一张图的事实是子图如此有用的原因之一:它们的工作原理与更大的图一样,但内存占用更少
-
7.2.2. 子图是以图的形式返回的,所以一旦为子图创建了图遍历源,就可以遍历这些子图并执行你学到的所有操作
7.3. 提取子图
-
7.3.1. 按顶点归纳子图是通过一组顶点及其之间的边来定义的
-
7.3.1.1. 按顶点归纳子图包括顶点之间的所有边
-
7.3.2. 按边归纳子图是通过一组边及其相邻顶点来定义的
-
7.3.2.1. 按边归纳子图只包括那些被定义的边
-
7.3.2.2. 使用边来定义范围似乎有违直觉
> 7.3.2.2.1. 从历史上看,我们在考虑数据时总是抱着一种实体第一(甚至是实体唯一)的思维方式
-
7.3.2.3. 图的实体关系允许我们使用边作为“一等公民”来定义子图的限度
-
7.3.2.4. Gremlin Server的TinkerPop实现支持按边归纳子图,因此按边归纳子图将是个性化用例的重点
-
7.3.3. 两种方法的结果并非总是不同
-
7.3.3.1. 子图的组成取决于你使用的方法以及在选择过程中使用的规则
-
7.3.4. subgraph(sideEffectKey):在一组较大的图数据中定义一个按边归纳子图
-
7.3.4.1. sideEffectKey是对副作用完整结果的引用
-
7.3.4.2. 副作用是我们改变状态的方式
-
7.3.4.3. subgraph()操作的主要作用是返回作为其输入的边
-
7.3.4.4. 该操作的副作用部分将这些相同的边及其相邻顶点添加到了由标签标识的内部集合中
-
7.3.4.5. TinkerPop的subgraph()操作有一个关键的限制:Gremlin语言变体(GLV)不支持它
> 7.3.4.5.1. GLV没有包括局部图的概念,这意味着我们不能返回子图,然后将其用于进一步的遍历> 7.3.4.5.2. GLV不支持子图,因此必须使用脚本提交方法来参数化并拼接字符串以编写遍历
-
7.3.4.6. subgraph变量只存在于会话中的服务器上
-
7.3.5. cap(sideEffectKey):向上迭代遍历到自身,并发出sideEffectKey引用的副作用结果
7.4. 遍历子图
-
7.4.1. Graph是一个数据存储
-
7.4.1.1. 只是存放数据的地方,除了最简单的查找操作以外,没有访问数据的其他能力
-
7.4.2. GraphTraversalSource是编写所有遍历的基础(遍历中的g)
-
7.4.2.1. 没有GraphTraversalSource的图对象就相当于没有任何类型文件管理器的文件系统及其文件
-
7.4.2.2. 没有任何类型的工具来导航文件系统、读取文件及其属性或者移动文件
> 7.4.2.2.1. 在使用子图之前,需要获得遍历源
7.5. 将子图用于串行隔离
-
7.5.1. 可以将其视为使用可序列化隔离模式与图数据交互的方式
-
7.5.2. 子图是在图数据库中建立可序列化隔离的一种无奈方式
-
7.5.3. 子图是没有磁盘缓存功能的内存结构,因此创建整个原始图的子图可能会造成内存压力,甚至会引发内存不足错误
-
7.5.4. 根据可串行隔离的定义,子图中发生的任何变化都不会反映在原始图中,反之亦然
-
7.5.4.1. 要依靠应用程序开发人员来协调两者之间的变化
-
7.5.5. 子图可以被复用甚至修改,但所做的任何更改都是与原始图隔离的
-
7.5.5.1. 意味着任何更改都不会传回原始图数据
7.6. 遍历末尾还包含了文本; null
-
7.6.1. 分号(;)用于结束第一个语句,即子图变量的赋值
-
7.6.2. null用于将整个操作返回给客户端
8. 反转遍历方向
8.1. 在大多数关系数据库建模中,特别是使用第三范式时,外键被设计为只在一个方向上使用
- 8.1.1. 如果要支持相反方向的连接,通常代价很高
8.2. 对于大多数图数据库来说,支持相反方向的连接不会产生额外的性能成本
8.3. 在关系数据库世界中,简单地改变关系的方向几乎是闻所未闻的,但对于图数据库来说,这几乎是一个微不足道的变化