GraphGists

本示例旨在展示图数据库如何成为以非常自然的方式管理家谱的环境。

我将处理马匹的家谱,这些家谱通常由一个机构在育马登记簿中管理,该机构必须确保信息的正确性和数据符合特定条件。

本示例将展示

  • 如何在育马登记簿中注册马匹,

  • 如何检查一些一致性条件,

  • 如何管理马匹与马主、育马者和承租人的关系,

  • 如何管理马匹的购买,以及

  • 如何从育马登记簿中查询信息,例如马匹的血统、祖先或后代。

我们假设此处马匹仅在育马登记簿中注册了一些基本数据:名称、出生年份、性别、毛色。我们还假设所有马匹都是纯血马,因此我们不必担心品种的兼容性。还可以注册许多其他数据,例如比赛和阿拉伯血统的百分比(当育马登记簿用于杂交马,例如英阿拉伯马时),国籍、出生日期和地点等:这些数据可以在更扩展的版本中进行管理。

如果母马(如果为雌性)或种马(如果为雄性)可以生育后代,则可以将其注册为育种马,即使它们没有生育后代:成为母马或种马是一种资格,马匹可以通过在育马登记簿中注册为育种马来获得该资格。

初始上传

可以使用以下指令执行已注册马匹数据库的初始上传

  • 创建马匹实例

CREATE (h1:Horse {id: $registrationId,  name: $horseName, birth_year: toInteger($birthYear), gender: $gender, mantle: $mantle })

其中$gender可以是“F”或“M”,$registrationId是识别育马登记簿中马匹的代码。

  • 注册为育种马

对于母马

MATCH (h1:Horse {name: $horseName})
SET h1:Mare

或种马

MATCH (h1:Horse {name: $horseName})
SET h1:Stud
  • 创建亲子关系

MATCH (m1:Mare {name: $MareName}) MATCH (h1:Horse {name: $horseName})
MATCH (s1:Stud {name: $StudName}) MATCH (h1:Horse {name: $horseName})
MERGE (m1)-[:DAM_OF]->(h1)
MERGE (s1)-[:SIRE_OF]->(h1)

以下是一个上传一些数据以开始的脚本

CREATE INDEX FOR (n:Horse) ON (n.name)
CREATE INDEX FOR (n:Mare) ON (n.name)
CREATE INDEX FOR (n:Stud) ON (n.name)

初始数据给出以下图谱

25ff7d35 1161 42a1 8720 59c97ae2e67b pu9QiwM18

一致性检查

为了随着时间的推移保持数据的正确性,建议使用一些查询来定期验证是否存在异常情况。它们在从现有数据库大规模上传数据后也必不可少:当所有这些查询均不返回结果时,数据一致。

  • 不存在循环

MATCH (parent)-[*]->(parent)
RETURN COUNT(parent)
  • 同一匹马不存在两个母马或两个种马

MATCH (dam1:Mare)-->(h:Horse), (dam2:Mare)-->(h)
WHERE exists((dam1)-->(h)<--(dam2))
RETURN DISTINCT h.name, dam1.name, dam2.name
MATCH (sire1:Stud)-->(h:Horse), (sire2:Stud)-->(h:Horse)
WHERE exists((sire1)-->(h)<--(sire2))
RETURN DISTINCT h.name, sire1.name, sire2.name
  • 同一母马在同一年不存在两个儿子

MATCH (horse1:Horse)<--(dam:Mare)-->(horse2:Horse)
WHERE horse1.birth_year = horse2.birth_year
RETURN DISTINCT dam.name, horse1.name, horse2.name
  • 不存在对于其儿子来说太年轻的母马或种马

MATCH (parent:Horse)-->(son:Horse)
WHERE parent.birth_year >= son.birth_year - 2
RETURN DISTINCT parent.name, parent.birth_year, son.name, son.birth_year
  • 不存在对于其儿子来说太老的母马

MATCH (dam:Mare)-->(son:Horse)
WHERE dam.birth_year < son.birth_year - 20
RETURN DISTINCT dam.name, dam.birth_year, son.name, son.birth_year

至于种马,如果育马登记簿规则允许使用人工授精,它们甚至可以在晚年产下后代;否则,也必须对种马执行类似的检查。

如何管理数据

现在让我们看看在允许我们管理育马登记簿数据的应用程序中应该输入什么。

第一个功能是将新马匹(小马驹)注册到育马登记簿中。

  • 在育马登记簿的育种部分注册

在产生有资格注册的小马驹之前,未来的母亲(母马)和未来的父亲(种马)必须分别在育马登记簿的相应部分注册为育种马,即母马种马

对于母马,注册为育种马的功能必须执行以下指令

MATCH (h:Horse {name: $name})
WHERE h.gender = 'F' AND NOT h:Mare
SET h:Mare
RETURN h.name as MareName, labels(h) as Labels

对于种马,需要以下指令

MATCH (h:Horse {name: $name})
WHERE h.gender = 'M' AND NOT h:Stud
SET h:Stud
RETURN h.name as StudName, labels(h) as Labels

在这两个指令中,条件确保我们

  1. 马匹的性别正确

  2. 马匹尚未在登记簿中

只有在两个条件都满足的情况下,才会执行注册。

  • 在育马登记簿中注册小马驹

小马驹出生后,只有在其母马和种马都已注册的情况下才能在育马登记簿中注册。因此,必须为这种操作执行的指令如下

MATCH (sire:Stud {name: $sireName})
MATCH (dam:Mare {name: $dameName})
OPTIONAL MATCH (dam)<-[:SIRE_OF]-(damssire)
CREATE (foal:Horse {id: $registrationId,  name: $foalName, birth_year: toInteger($birthYear), gender: $gender, mantle: $mantle})
CREATE (dam)-[:DAM_OF]->(foal)
CREATE (sire)-[:SIRE_OF]->(foal)
RETURN 'Foal registered: ' + foal.name + ' by ' + sire.name + ' out of ' + dam.name + ' (' + damssire.name + ')' as NewFoal

让我们看一下该指令。首先,如果母马或种马未注册为育种马,则相应的 MATCH 将没有结果(标签过滤器),并且小马驹的注册失败。母马种马的 OPTIONAL MATCH 对于避免匹配在并非所有母马数据都立即可用时(即如果她是导入的或在重建家谱的情况下)失败是必要的。

  • 模型中的人类:育马者、马主和承租人

关于马匹,一个人可以扮演的主要角色是

  • 育马者:让马匹出生并至少在最初一段时间内饲养它;

  • 马主:拥有马匹的所有权,不一定与育马者相同;

  • 承租人:从马主那里获得马匹的临时所有权。

一方面,公证书足以证明一个人是否是马匹的马主或承租人。另一方面,一个人作为产下小马驹的母马的马主或承租人成为育马者。因此,一个人可以与马匹拥有的角色源于在人和马匹之间建立的关系。

在出生时,母马的马主或承租人自动成为小马驹的育马者或马主:然后,之前为小马驹注册看到的指令必须以以下方式完成

MATCH (sire:Stud {name: $sireName})
MATCH (dam:Mare {name: $dameName})
OPTIONAL MATCH (dam)<-[:SIRE_OF]-(damssire)
CREATE (foal:Horse {name: $foalName, birth_year: toInteger($birtYear), gender: $gender, mantle: $mantle})
CREATE (dam)-[:DAM_OF]->(foal)
CREATE (sire)-[:SIRE_OF]->(foal)
WITH sire, dam, damssire, foal
MATCH (dam)<-[ownshp:OWNER_OF]-(owner)
OPTIONAL MATCH (dam)<-[tenshp:TENANT_OF]-(tenant)
WITH sire, dam, damssire, foal, coalesce(tenant, owner) as breeder, coalesce(tenshp, ownshp) as quote
CREATE (breeder)-[:BREEDER_OF {breed_perc: quote.property_perc}]->(foal)
CREATE (breeder)-[:OWNER_OF {property_perc: quote.property_perc}]->(foal)
RETURN DISTINCT 'Foal registered: ' + foal.name + ' by ' + sire.name + ' out of ' + dam.name + ' (' + damssire.name + ')' as NewFoal

让我们仔细看看。在创建新的小马驹及其与父母的关系后,脚本继续(第一个 WITH)获取母马的马主和承租人(如果有)(OPTIONAL 子句);然后(第二个 WITH)选择承租人或马主作为小马驹的育马者,无论如何都使用各自的财产百分比(变量quote),并最终创建财产和育种关系。返回的字符串具有马匹命名的典型形式,包括种马、母马和母马的种马。显然,许多人可以是马匹的马主或承租人:即使在这种情况下,脚本也能完美运行。

为了检查人与马匹之间所有关系是否一致,即每种关系类型的百分比总和对于所有马匹而言均为 100,必须将以下语句添加到要实现的一致性检查中

MATCH (p:Person)-[r:OWNER_OF]->(h:Horse)
WITH h, sum(r.property_perc) as sum_property_perc
WHERE sum_property_perc <> 100
RETURN h, sum_property_perc

对于其他类型的关系(:BREEDER_OF、:TENANT_OF),也类似。

  • 购买马匹

如果购买马匹,则可以使用以下指令获得财产权的新配置(此处新马主为三人,但可以从一人到 n 人)

MATCH (h:Horse {name: $horseName})
OPTIONAL MATCH (h)<-[oldOwnshp:OWNER_OF]-()
DELETE oldOwnshp
WITH [{name:$newowner1, property_perc: toFloat($perc1)}
    , {name:$newowner2, property_perc: toFloat($perc2)}
    , {name:$newowner3, property_perc: toFloat($perc3)}
] AS purchaserList
UNWIND purchaserList AS purchaser
MERGE (p:Person {name: purchaser.name})
ON CREATE SET p.property_perc = purchaser.property_perc
ON MATCH SET p.property_perc = purchaser.property_perc
MERGE (p)-[newOwnshp:OWNER_OF]->(h)
SET newOwnshp.property_perc = p.property_perc
REMOVE p.property_perc
RETURN h.name, collect(p.name)

首先,删除旧的所有权(如果存在)。在 WITH 语句中,列出了新马主,每个马主都有其财产百分比。然后,在 UNWIND 中列出以获取新马主的表。对于每个新马主(数据库中已存在或新的),都会暂时将财产百分比分配给他/她;当创建每个新马主与马匹之间的新的关系时,百分比将分配给该关系,并且所有权中的临时值将被删除。

可以为租赁编写类似的指令,而育种权通常不能出售。

让我们看看 Cypher 的实际应用

我们现在将把刚刚看到的指令应用于最初输入的数据。

让我们添加一些与三匹已注册马匹相关的人员:他们的育马者和马主,以及其中一匹马的承租人

MATCH (h1:Horse {name: 'Scorpius'})
MATCH (h2:Horse {name: 'Tucana'})
MATCH (h3:Horse {name: 'Aries'})
CREATE (p1:Person {name: 'Julia Stokes'})
CREATE (p2:Person {name: 'Hugh Kelley'})
CREATE (p3:Person {name: 'Anne Nicholson'})
CREATE (p4:Person {name: 'Jeremy Dalton'})
CREATE (p5:Person {name: 'Beatrice Fowler'})
CREATE (p1)-[:BREEDER_OF {breed_perc: toFloat(100.0)}]-> (h1)
CREATE (p1)-[:OWNER_OF {property_perc: toFloat(100.0)}]-> (h1)
CREATE (p2)-[:BREEDER_OF {breed_perc: toFloat(100.0)}]-> (h2)
CREATE (p3)-[:OWNER_OF {property_perc: toFloat(60.0)}]-> (h2)
CREATE (p4)-[:OWNER_OF {property_perc: toFloat(40.0)}]-> (h2)
CREATE (p5)-[:BREEDER_OF {breed_perc: toFloat(100.0)}]-> (h3)
CREATE (p5)-[:OWNER_OF {property_perc: toFloat(100.0)}]-> (h3)
CREATE (p3)-[:TENANT_OF {rental_perc: toFloat(100.0)}]-> (h3)
RETURN h1,h2,h3,p1,p2,p3,p4,p5

情况如下,其中所有三匹马都为蓝色,因为它们都还没有成为育种马

MATCH (h1:Horse {name: 'Scorpius'})
MATCH (h2:Horse {name: 'Tucana'})
MATCH (h3:Horse {name: 'Aries'})
MATCH (p1)-[r1:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h1)
MATCH (p2)-[r2:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h2)
MATCH (p3)-[r3:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h3)
RETURN h1,h2,h3,p1,p2,p3,r1,r2,r3
0affb127 1610 4456 adae d7fc964a613f ECj2NRDdg

为了得到一匹新的小马驹,Tucana 和 Scorpius 必须成为育种马

MATCH (h:Horse {name: 'Tucana'})
WHERE h.gender = 'F' AND NOT h:Mare
SET h:Mare
RETURN h.name as MareName, labels(h) as Labels
MATCH (h:Horse {name: 'Scorpius'})
WHERE h.gender = 'M' AND NOT h:Stud
SET h:Stud
RETURN h.name as StudName, labels(h) as Labels

现在的情况如下:天蝎座(Scorpius)像种马一样是红色的,杜鹃座(Tucana)像母马一样是紫色的。

MATCH (h1:Horse {name: 'Scorpius'})
MATCH (h2:Horse {name: 'Tucana'})
MATCH (h3:Horse {name: 'Aries'})
MATCH (p1)-[r1:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h1)
MATCH (p2)-[r2:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h2)
MATCH (p3)-[r3:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h3)
RETURN h1,h2,h3,p1,p2,p3,r1,r2,r3
fe3af6c9 7aac 44fe 9aa3 59a00c6aed86 nVPAhvF28

假设杜鹃座和天蝎座产下一匹小马,名叫土星(Saturn)。

MATCH (sire:Stud {name: 'Scorpius'})
MATCH (dam:Mare {name: 'Tucana'})
OPTIONAL MATCH (dam)<-[:SIRE_OF]-(damssire)
CREATE (foal:Horse {name: 'Saturn', birth_year: toInteger(2018), gender: 'M', mantle: 'bay'})
CREATE (dam)-[:DAM_OF]->(foal)
CREATE (sire)-[:SIRE_OF]->(foal)
WITH sire, dam, damssire, foal
MATCH (dam)<-[ownshp:OWNER_OF]-(owner)
OPTIONAL MATCH (dam)<-[tenshp:TENANT_OF]-(tenant)
WITH sire, dam, damssire, foal, coalesce(tenant, owner) as breeder, coalesce(tenshp, ownshp) as quote
CREATE (breeder)-[:BREEDER_OF {breed_perc: quote.property_perc}]->(foal)
CREATE (breeder)-[:OWNER_OF {property_perc: quote.property_perc}]->(foal)
RETURN DISTINCT 'Foal registered: ' + foal.name + ', by ' + sire.name + ' out of ' + dam.name + ' (' + damssire.name + ')' as NewFoal

新的情况如下:

MATCH (h1:Horse {name: 'Scorpius'})
MATCH (h2:Horse {name: 'Tucana'})
MATCH (h3:Horse {name: 'Aries'})
MATCH (h4:Horse {name: 'Saturn'})
MATCH (p1)-[r1:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h1)
MATCH (p2)-[r2:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h2)
MATCH (p3)-[r3:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h3)
MATCH (p4)-[r4:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h4)
MATCH (h1)-[r5:SIRE_OF]->(h4)<-[r6:DAM_OF]-(h2)
RETURN h1,h2,h3,h4,p1,p2,p3,p4,r1,r2,r3,r4
0a1a64b7 9f64 4dd4 b8f0 d2bf3b364eac phQPfAt1k

您可以看到,土星的育种者和所有者是其母马杜鹃座的所有者,安妮·尼科尔森(Anne Nicholson)和杰里米·道尔顿(Jeremy Dalton),他们拥有母马的相同百分比的权利,分别为 60% 和 40%。

MATCH (h:Horse {name: 'Saturn'})
MATCH (h)<-[r:OWNER_OF]-(p)
RETURN p.name, r.property_perc
MATCH (h:Horse {name: 'Saturn'})
MATCH (h)<-[r:BREEDER_OF]-(p)
RETURN p.name, r.breed_perc

现在假设这匹新的小马卖给了茱莉亚·斯托克斯(Julia Stokes)、休·凯利(Hugh Kelley)和菲利普·林赛(Philip Lindsey)(最后一位是数据库中的新成员),他们分别拥有 25%、25% 和 50% 的产权。

MATCH (h:Horse {name: 'Saturn'})
OPTIONAL MATCH (h)<-[oldOwnshp:OWNER_OF]-()
DELETE oldOwnshp
WITH DISTINCT h, [{name:'Julia Stokes', property_perc: toFloat(25)}, {name:'Hugh Kelley', property_perc: toFloat(25)}, {name:'Philip Lindsey', property_perc: toFloat(50)}] AS purchaserList
UNWIND purchaserList AS purchaser
MERGE (p:Person {name: purchaser.name})
ON CREATE SET p.property_perc = purchaser.property_perc
ON MATCH SET p.property_perc = purchaser.property_perc
CREATE (p)-[newOwnshp:OWNER_OF {property_perc: p.property_perc}]->(h)
REMOVE p.property_perc
RETURN p.name, type(newOwnshp), newOwnshp.property_perc

最终情况如下:

MATCH (h1:Horse {name: 'Scorpius'})
MATCH (h2:Horse {name: 'Tucana'})
MATCH (h3:Horse {name: 'Aries'})
MATCH (h4:Horse {name: 'Saturn'})
MATCH (p1)-[r1:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h1)
MATCH (p2)-[r2:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h2)
MATCH (p3)-[r3:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h3)
MATCH (p4)-[r4:BREEDER_OF|OWNER_OF|TENANT_OF]-> (h4)
MATCH (h1)-[]->(h4)<-[]-(h2)
RETURN h1,h2,h3,h4,p1,p2,p3,p4,r1,r2,r3,r4
8ca2cd1a 5df1 4a39 8b6d 7e02c65bb08f s3Bj1FFau

我们可以检查产权是否正确。

MATCH (h:Horse {name: 'Saturn'})
MATCH (h)<-[r:OWNER_OF]-(p)
RETURN p.name, r.property_perc

没错!

通过看到的说明,我们可以构建一个应用程序,让我们以自然且简单的方式管理育种记录(Studbook)。

数据查询

用户可以向育种记录提出的典型查询与有关马匹、其祖先和后代、拥有或管理它的用户信息相关的查询。让我们看看其中一些查询。

  • 马匹血统

血统的主要部分显示了马匹的数据以及父母和祖父母的数据(通常也包括曾祖父母的数据)。

MATCH (h:Horse {name: 'Saturn'})
OPTIONAL MATCH (h)<-[:DAM_OF]-(dam:Mare)
OPTIONAL MATCH (h)<-[:SIRE_OF]-(sire:Stud)
OPTIONAL MATCH (dam)<-[:DAM_OF]-(damsdam:Mare)
OPTIONAL MATCH (dam)<-[:SIRE_OF]-(damssire:Stud)
OPTIONAL MATCH (sire)<-[:DAM_OF]-(siresdam:Mare)
OPTIONAL MATCH (sire)<-[:SIRE_OF]-(siressire:Stud)
RETURN h.name as Name, h.gender as Gender, h.birth_year as Birth_year, h.mantle as Mantle, sire.name as Sire_name, sire.birth_year as Sire_birth_year, sire.mantle as Sire_mantle, siressire.name as Sire_of_sire, siresdam.name as Dam_of_sire, dam.name as Dam_name, dam.birth_year as Dam_birth_year, dam.mantle as Dam_mantle, damssire.name as Sire_of_dam, damsdam.name as Dam_of_dam
  • 母系血统

血统的另一个典型部分与母系血统相关,或者说是母系祖先的列表,直到第三代(或第四代)。

MATCH (h:Horse {name: 'Saturn'})
OPTIONAL MATCH (h)<-[:DAM_OF]-(d1:Horse)
OPTIONAL MATCH (d1)<-[:DAM_OF]-(d2:Horse)
OPTIONAL MATCH (d2)<-[:DAM_OF]-(d3:Horse)
RETURN h.name AS Name, h.birth_year AS Birth, d1.name AS FirstMother, d1.birth_year AS FMBirth, d2.name AS SecondMother, d2.birth_year AS SMBirth, d3.name AS ThirdMother, d3.birth_year AS TMBirth
  • 马匹后代

显然,如果一匹马有后代,了解它们有多少以及来自哪些父母是很重要的。

MATCH (h:Horse {name: 'Cepheus'})
OPTIONAL MATCH (h)-->(d:Horse)
OPTIONAL MATCH (d)<--(p:Horse) WHERE p.name <> h.name
RETURN h.name as Name, h.gender as Gender, h.birth_year as Birth, h.mantle as Mantle, d.name as Descendant, d.gender as DGender, d.birth_year as DBirth, p.name as DParent, p.birth_year as DParent_birth
ORDER BY d.birth_year
MATCH (h:Horse {name: 'Virgo'})
OPTIONAL MATCH (h)-->(d:Horse)
OPTIONAL MATCH (d)<--(p:Horse) WHERE p.name <> h.name
RETURN h.name as Name, h.gender as Gender, h.birth_year as Birth, h.mantle as Mantle, d.name as Descendant, d.gender as DGender, d.birth_year as DBirth, p.name as DParent, p.birth_year as DParent_birth
ORDER BY d.birth_year

结论

我们已经看到,在图上查看育种记录数据是多么容易和自然,以及管理它们的说明是多么清晰。

希望您喜欢这个要点,并能从中获得一些关于真实应用程序的提示。正如我之前所说,数据模型可以得到很大程度的增强,例如为实例添加其他属性或重构某些方面,例如时间或国籍(出生年份或国家作为节点):从重构的角度来看,您会发现 Neo4j 允许您以比关系型数据库更流畅和灵活的方式增强节点和关系的信息,一方面保留了真正关系型结构的优势,另一方面享受了自由结构数据库的优势。

祝您玩得愉快!