Neo4j如何为时间版本的图形建模

时间:2017-08-14 08:01:01

标签: graph neo4j cypher

我的部分图表有以下架构:

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的所有数据。

现在问题出现了,因为来自生产数据库的用户可以更改他的电子邮件。所以我现在导入这个的方式我将最终得到两个具有相同user_id的人,随后所有设备/日历将链接到两个人,因为他们都共享相同的user_id。所以这不是对现实的准确表述。 我们还需要捕获设备与特定user_id的连接/断开连接,因为可以连接/断开设备并将其借给具有不同管理员(user_id)的朋友。

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

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

2 个答案:

答案 0 :(得分:9)

这个答案基于Ian Robinson关于time-based versioned graphs的帖子。

我不知道这个答案是否涵盖所有问题的要求,但我相信这可以提供一些见解。

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

初始图形状态:

考虑此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是当前的管理员。 fromto关系中的属性:ADMIN:CONNECTED_DEVICE用于管理图结构的历史记录。 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}│0     │1000│
└───────────────┴───────────────┴──────┴────┘

上述结果显示person_id = 1已连接到开头的device_id = 1,直到今天。

更改图表结构

考虑当前时间点为30.现在user_id = 1正在与device_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) 

结果图将是:

Graph after structural change

在此结构更改后,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}│0     │30  │
├───────────────┼───────────────┼──────┼────┤
│{"person_id":2}│{"device_id":1}│31    │1000│
└───────────────┴───────────────┴──────┴────┘

上述结果显示,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}│
└───────────────┘

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

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

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

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

答案 1 :(得分:2)

解析GUID

您需要的第一件事是可靠地解析用户ID,以使它们一致且全局唯一。现在你说了

  

用户ID仅适用于生产数据库,不是全局的   用于其他来源

由此,我可以推断出两件事

  1. 用户来自多个来源。
  2. 对于每个来源,用户都有唯一的ID。
  3. 这意味着source + user.id将成为GUID。 (您可以散列主连接URL或在外部命名每个源)我将假设您没有跨多个源合并用户,因为通过任何网络复制和合并数据会产生应尽可能避免的更新顺序悖论(如果两个来源列出了不同的新联系号码,谁是正确的?)。

    查询当前数据

    查询逻辑应该与您可能正在进行的任何版本跟踪无关。如果您的版本控制导致逻辑问题,请添加带有索引属性:Versioned的{​​{1}}等元标签,并添加isLatest以过滤掉结果中的旧“垃圾”数据。< / p>

    所以不,你不需要担心版本,查询1和2可以正常处理。

    1. 为了找到管理员,我建议您只为该人员添加标签Where n.isLatest,并在不再适用时将其删除(根据需要)。这是通过标签“Admin”索引。您也可以使用“isAdmin”属性(这可能是您已经将它存储在数据库中的方式,因此更加一致。)因此,最终查询只能是:AdminMATCH (p:Person:Admin)

    2. 过滤掉旧版本信息后,对拥有设备的人的查询只会是MATCH (p:Person{isAdmin:true})

    3. 这一点真的只归结为“你的架构是什么?”

      数据历史

      这一点真的很棘手。根据您对数据的版本设置,您可以轻松地最终耗尽数据大小并终止数据库性能。你真的需要问问自己“我为什么要对它进行版本化?”,“这次更新/阅读的频率是多少?”,“谁将使用它以及它们将用它做什么?”。如果您在任何时候回答“我不知道/关心”,您要么不应该这样做,要么将数据备份到本地为您处理的数据库中SQLAlchemy-Continuum。 (相关answer

      如果你必须在Neo4j中这样做,那么我建议使用delta链。因此,例如,如果您将MATCH (p:Person:Versioned{isCurrent:true})-[:HasDevice{isConnected:true}]->(d:Device:Versioned{isCurrent:true})更改为{a:1, b:2},那么您将拥有{a:1, b:null, c:3}。这样,要获得过去的值,您只需将delta链的属性链接到地图中即可。所以(:Thing{a:1, b:null, c:3})-[_DELTA{timestamp:<value>}]->(:_ThingDelta{b: 2, c:null})。这可能会变得非常繁琐并占用你的数据库空间,所以如果可以的话,我强烈建议不惜一切代价使用一些现有的db版本。