Neo4j如何建模一个时间版本的图形数据结构

13

我的图表的一部分具有以下结构:

enter image description here

图表的主要部分是域,其中有一些人与之关联。人员在电子邮件属性上具有唯一约束条件,因为我还从其他来源获得了数据,并且这很适合。

在我的情况下,人员可以成为管理员,他拥有一些设备/日历与之关联。我从SQL数据库中获取此数据,在其中导入几个表以组合整个图片。我从一个具有两列的表开始,管理员的电子邮件和用户ID。此用户ID仅针对生产数据库特定,并且不用于全局用于其他来源。这就是为什么我将电子邮件用作人员的全局标识符。我目前使用以下查询导入所有生产表都链接到的用户ID。我始终获取用户设置和信息的当前快照。此查询每天运行4次:

CALL apoc.load.jdbc(url, import_query) yield row
MERGE (p:Person{email:row.email})
SET p.user_id = row.id

然后,我会从其他表格中导入与该用户ID相关联的所有数据。

现在问题出现了,因为生产数据库中的用户可以更改他的电子邮件地址。所以我现在导入数据的方式将导致两个人拥有相同的用户ID,随后所有设备/日历都将链接到这两个人,因为他们共享相同的用户ID。因此,这并不是对实际情况的准确描述。 我们还需要通过时间捕获将设备连接/断开到特定用户ID,因为一个人可以连接/断开设备并将其借给具有不同管理员(用户ID)的朋友。

如何更改我的图形模型(导入查询),以便:

  1. 查询当前管理员不需要复杂的查询
  2. 查询当前连接的设备不需要复杂的查询
  3. 查询历史记录可能会更加复杂。

2
根据您的声誉,我假设您已经研究了几种解决方案。所以请原谅我如果听起来很愚蠢。用户ID与电子邮件的难题很棘手。如果两者都不能作为唯一标识符使用,那么我会选择其他可以的东西。至于版本控制...一个经常使用的解决方案(尽管在我看来不太美观)是将版本放在关系类型中。因此,例如,CONNECTED_DEVICE_20170814关系将告诉您今天连接了什么。通常,当前关系由没有版本的类型给出(这里是CONNECTED_DEVICE)。 - Tom Geudens
1
看起来最好将用户节点从人节点中分离出来,这样我们就可以得到(:Person{email})-[:ADMIN|:WAS_ADMIN]-(:AdminNode{user_id})。一个选择是将当前状态保存为单独的关系类型ADMIN或CONNECTED_DEVICE,历史记录保存为WAS_ADMIN。然后设置一个定时任务根据关系的最新时间戳将关系类型从:ADMIN更改为:WAS_ADMIN。我正在查看是否有更好的版本 :) - Tomaž Bratanič
如果用户ID是用户表的主键,那么在合并时应该使用它而不是电子邮件。虽然电子邮件不是全局唯一的,但它可以确保来自用户表的是同一个用户。 - szenyo
2个回答

13

这篇回答基于Ian Robinson关于时间版本化图的文章

我不知道这篇回答是否覆盖了问题的所有要求,但我相信它可以提供一些见解。

此外,我认为您只对结构性版本控制感兴趣(也就是说:您不关心关于域用户名称随时间变化的查询)。最后,我使用了您图模型的部分表示,但我相信这里展示的概念可以应用于整个图表。

初始图状态:

考虑以下Cypher语句创建一个初始图状态:

CREATE (admin:Admin)

CREATE (person1:Person {person_id : 1})
CREATE (person2:Person {person_id : 2})
CREATE (person3:Person {person_id : 3})

CREATE (domain1:Domain {domain_id : 1})

CREATE (device1:Device {device_id : 1})

CREATE (person1)-[:ADMIN {from : 0, to : 1000}]->(admin)

CREATE (person1)-[:CONNECTED_DEVICE {from : 0, to : 1000}]->(device1)

CREATE (domain1)-[:MEMBER]->(person1)
CREATE (domain1)-[:MEMBER]->(person2)
CREATE (domain1)-[:MEMBER]->(person3)

结果:

Initial Graph state

上面的图形有3个人节点。这些节点都是一个域节点的成员。具有person_id = 1的人节点连接到具有device_id = 1的设备。同时,person_id = 1是当前管理员。在:ADMIN:CONNECTED_DEVICE关系中的属性fromto被用来管理图形结构的历史记录。 from代表时间的起点,to代表时间的终点。为了简化,我使用0作为图形的初始时间,1000作为结束时间常量。在真实世界的图形中,毫秒级别的当前时间可以用于表示时间点。此外,Long.MAX_VALUE也可以用作EOT常量。一个带有to = 1000的关系意味着与之相关的周期没有当前的上限。

查询:

使用此图形,要获取当前管理员,可以执行以下操作:

MATCH (person:Person)-[:ADMIN {to:1000}]->(:Admin)
RETURN person

结果将是:

╒═══════════════╕
│"person"       │
╞═══════════════╡
│{"person_id":1}│
└───────────────┘
给定一个设备,获取当前连接的用户:
MATCH (:Device {device_id : 1})<-[:CONNECTED_DEVICE {to : 1000}]-(person:Person)
RETURN person

结果如下:

╒═══════════════╕
│"person"       │
╞═══════════════╡
│{"person_id":1}│
└───────────────┘

使用 End-Of-Time 常量来查询当前管理员和设备上当前连接的用户。

查询设备连接/断开事件:

MATCH (device:Device {device_id : 1})<-[r:CONNECTED_DEVICE]-(person:Person)
RETURN person AS person, device AS device, r.from AS from, r.to AS to
ORDER BY r.from

结果为:

╒═══════════════╤═══════════════╤══════╤════╕
│"person""device""from""to"│
╞═══════════════╪═══════════════╪══════╪════╡
│{"person_id":1}{"device_id":1}01000│
└───────────────┴───────────────┴──────┴────┘

以上结果表明,person_id = 1 从一开始直到现在都连接着 device_id = 1

更改图形结构

考虑当前时刻为30。现在 user_id = 1device_id = 1 断开连接,user_id = 2 连接上它。为了表示这种结构性变化,我将运行以下查询:

// Get the current connected person
MATCH (person1:Person)-[old:CONNECTED_DEVICE {to : 1000}]->(device:Device {device_id : 1})
// get person_id = 2
MATCH (person2:Person {person_id : 2}) 
 // set 30 as the end time of the connection between person_id = 1 and device_id = 1
SET old.to = 30
// set person_id = 2 as the current connected user to device_id = 1
// (from time point 31 to now)
CREATE (person2)-[:CONNECTED_DEVICE {from : 31, to: 1000}]->(device) 

生成的图表如下:

结构变化后的图表

进行这次结构变化之后,device_id = 1 的连接历史将如下所示:

MATCH (device:Device {device_id : 1})<-[r:CONNECTED_DEVICE]-(person:Person)
RETURN person AS person, device AS device, r.from AS from, r.to AS to
ORDER BY r.from

╒═══════════════╤═══════════════╤══════╤════╕
│"person""device""from""to"│
╞═══════════════╪═══════════════╪══════╪════╡
│{"person_id":1}{"device_id":1}030  │
├───────────────┼───────────────┼──────┼────┤
│{"person_id":2}{"device_id":1}311000│
└───────────────┴───────────────┴──────┴────┘
上述结果表明,user_id = 1在0到30时间内连接到device_id = 1person_id = 2目前连接到device_id = 1
现在连接到device_id = 1的当前用户是person_id = 2
MATCH (:Device {device_id : 1})<-[:CONNECTED_DEVICE {to : 1000}]-(person:Person)
RETURN person

╒═══════════════╕
│"person"       │
╞═══════════════╡
│{"person_id":2}│
└───────────────┘

相同的方法可以应用于管理管理员历史记录。

显然,这种方法有一些缺点:

  • 需要管理一组额外的关联关系
  • 查询更加昂贵
  • 查询更加复杂

但是,如果您真的需要一个版本控制模式,我相信这种方法是一个不错的选择或(至少)一个良好的起点。


2

解决GUID

首先需要可靠地解析用户ID,以使其始终保持一致且全局唯一。你说过:

用户ID仅针对生产数据库具有特定性,并不适用于其他来源。

从这可以推断出两件事:

  1. 用户存在于多个来源。
  2. 对于每个来源,用户都有唯一的ID。

因此,source + user.id 将是一个GUID(您可以哈希主连接URL或在外部命名每个来源)。我假设您不会跨多个来源合并用户,因为在任何网络上复制和合并数据会创建更新顺序悖论,应尽可能避免(如果两个来源列出不同的新联系电话号码,谁是正确的?)。

查询当前数据

查询逻辑应该与您可能正在进行的版本跟踪无关。如果您的版本控制会影响逻辑,请添加一个类似于:Versioned的元标签,带有索引属性isLatest, 并添加一个 Where n.isLatest来过滤结果中的旧“垃圾”数据。

所以现在,您不需要担心版本问题,查询1和查询2可以正常处理。

  1. 查找管理员的人,我建议只需将标签:Admin添加到该人,并在不再适用时(根据需要)将其删除。这与被标记为“Admin”的索引有关。您还可以只使用“isAdmin”属性(这可能是您已经在数据库中存储它的方式,因此更加一致)。因此最终查询就是MATCH (p:Person:Admin)MATCH (p:Person{isAdmin:true})

  2. 过滤掉旧版本信息后,查找拥有设备的人的查询将简单地成为MATCH (p:Person:Versioned{isCurrent:true})-[:HasDevice{isConnected:true}]->(d:Device:Versioned{isCurrent:true})

这部分真的只是“你的模式是什么?”

数据历史

这一部分真的很棘手。根据数据版本的不同,您可能会轻易地增加数据大小并且降低数据库性能。您真的需要问自己“为什么要对此进行版本控制?”,“这将有多频繁更新/读取?”,“谁会使用它,他们会用它做什么?”。如果您回答“我不知道/不关心”任何一个问题,那么您要么不应该这样做,要么在本地处理此类问题的数据库中备份数据,例如SQLAlchemy-Continuum。(相关答案
如果您一定要在Neo4j中执行此操作,则建议使用增量链。例如,如果您将{a:1, b:2}更改为{a:1, b:null, c:3},则会得到(:Thing {a:1,b:null,c:3})- [_DELTA {timestamp:<value>}]->(:_ThingDelta {b: 2,c:null})。这样,要获取过去的值,只需将增量链的属性链接到一个映射中即可。因此,MATCH (a:Thing) OPTIONAL MATCH (a)-[d:_DELTA*]->(d) WHERE d.timestamp >= <value> WITH reduce(v = {_id:ID(a)}, n IN nodes(p)| v += PROPERTIES(n)) AS OldVersion。但这可能会变得非常繁琐,并且占用您的数据库空间,因此如果可能的话,我强烈建议您尽一切可能使用现有的数据库版本控制工具。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接