在我的Neo4J数据库中,我有一系列通过双链表实现的卡片队列.数据结构显示在下图中(使用Alistair Jones的Arrows在线工具生成的队列的SVG图):
由于这些是队列,我总是从队列的TAIL添加新项目.我知道双关系(下一个/上一个)不是必需的,但它们简化了两个方向的遍历,所以我更喜欢它们.
这是我用来插入新"卡片"的查询:
MATCH (currentList:List)-[currentTailRel:TailCard]->(currentTail:Card) WHERE ID(currentList) = {{LIST_ID}} CREATE (currentList)-[newTailRel:TailCard]->(newCard:Card { title: {{TITLE}}, description: {{DESCRIPTION}} }) CREATE (newCard)-[newPrevRel:PreviousCard]->(currentTail) CREATE (currentTail)-[newNextRel:NextCard]->(newCard) DELETE currentTailRel WITH count(newCard) as countNewCard WHERE countNewCard = 0 MATCH (emptyList:List)-[fakeTailRel:TailCard]->(emptyList), (emptyList)-[fakeHeadRel:HeadCard]->(emptyList) WHERE ID(emptyList) = {{LIST_ID}} WITH emptyList, fakeTailRel, fakeHeadRel CREATE (emptyList)-[:TailCard]->(newCard:Card { title: {{TITLE}}, description: {{DESCRIPTION}} }) CREATE (emptyList)-[:HeadCard]->(newCard) DELETE fakeTailRel, fakeHeadRel RETURN true
查询可以分为两部分.在第一部分:
MATCH (currentList:List)-[currentTailRel:TailCard]->(currentTail:Card) WHERE ID(currentList) = {{LIST_ID}} CREATE (currentList)-[newTailRel:TailCard]->(newCard:Card { title: {{TITLE}}, description: {{DESCRIPTION}} }) CREATE (newCard)-[newPrevRel:PreviousCard]->(currentTail) CREATE (currentTail)-[newNextRel:NextCard]->(newCard) DELETE currentTailRel
我处理将卡添加到已有其他卡的队列的一般情况.在第二部分:
WITH count(newCard) as countNewCard WHERE countNewCard = 0 MATCH (emptyList:List)-[fakeTailRel:TailCard]->(emptyList), (emptyList)-[fakeHeadRel:HeadCard]->(emptyList) WHERE ID(emptyList) = {{LIST_ID}} WITH emptyList, fakeTailRel, fakeHeadRel CREATE (emptyList)-[:TailCard]->(newCard:Card { title: {{TITLE}}, description: {{DESCRIPTION}} }) CREATE (emptyList)-[:HeadCard]->(newCard) DELETE fakeTailRel, fakeHeadRel RETURN true
我处理队列中没有卡片的情况.在这种情况下,(emptyList)节点有两个类型为HeadCard和TailCard的关系,指向自身(我称之为假尾和假头).
这似乎有效.尽管如此,作为一个菜鸟,我有一种感觉,我正在过度思考事物,并且可能有更优雅和直接的方式来实现这一目标.例如,我想要了解如何以更好/更简单的方式做的一件事是如何分离两个子查询.如果可能的话,我也希望能够在两种情况下都返回新创建的节点.
以下是我从队列中删除节点的方法.我从不想简单地删除节点,我宁愿将它们添加到存档节点,以便在需要时可以恢复它们.我已经确定了这些案例:
当要归档的节点位于队列的中间时
// archive a node in the middle of a doubly-linked list MATCH (before:Card)-[n1:NextCard]->(middle:Card)-[n2:NextCard]->(after:Card) WHERE ID(middle)=48 CREATE (before)-[:NextCard]->(after) CREATE (after)-[:PreviousCard]->(before) WITH middle, before, after MATCH (middle)-[r]-(n) DELETE r WITH middle, before, after MATCH (before)<-[:NextCard*]-(c:Card)<-[:HeadCard]-(l:List)<-[:NextList*]-(fl:List)<-[:HeadList]-(p:Project)-[:ArchiveList]->(archive:List) CREATE (archive)-[r:Archived { archivedOn : timestamp() }]->(middle) RETURN middle
当要归档的节点是队列的头部时
// archive the head node of a doubly-linked list MATCH (list:List)-[h1:HeadCard]->(head:Card)-[n1:NextCard]->(second:Card) WHERE ID(head)=48 CREATE (list)-[:HeadCard]->(second) WITH head, list MATCH (head)-[r]-(n) DELETE r WITH head, list MATCH (list)<-[:NextList*]-(fl:List)<-[:HeadList]-(p:Project)-[:ArchiveList]->(archive:List) CREATE (archive)-[r:Archived { archivedOn : timestamp() }]->(head) RETURN head
当要归档的节点是队列的尾部时
// archive the tail node of a doubly-linked list MATCH (list:List)-[t1:TailCard]->(tail:Card)-[p1:PreviousCard]->(nextToLast:Card) WHERE ID(tail)=48 CREATE (list)-[:TailCard]->(nextToLast) WITH tail, list MATCH (tail)-[r]-(n) DELETE r WITH tail, list MATCH (list)<-[:NextList*]-(fl:List)<-[:HeadList]-(p:Project)-[:ArchiveList]->(archive:List) CREATE (archive)-[r:Archived { archivedOn : timestamp() }]->(tail) RETURN tail
当要归档的节点是队列中的唯一节点时
// archive the one and only node in the doubly-linked list MATCH (list:List)-[tc:TailCard]->(only:Card)<-[hc:HeadCard]-(list:List) WHERE ID(only)=48 CREATE (list)-[:TailCard]->(list) CREATE (list)-[:HeadCard]->(list) WITH only, list MATCH (only)-[r]-(n) DELETE r WITH only, list MATCH (list)<-[:NextList*]-(fl:List)<-[:HeadList]-(p:Project)-[:ArchiveList]->(archive:List) CREATE (archive)-[r:Archived { archivedOn : timestamp() }]->(only) RETURN only
我已经尝试过多种方法将以下cypher查询合并为一个,使用WITH语句,但我没有成功.我目前的计划是一个接一个地运行所有4个查询.实际上只有一个会做某事(即归档节点).
有什么建议可以让这更好,更精简吗?我甚至愿意重组数据结构,因为这是我为自己创建的沙盒项目,学习Angular和Neo4J,所以最终的目标是学习如何做得更好:)
也许数据结构本身可以改进?考虑到在队列末尾插入/存档节点有多复杂,我只能想象在队列中移动元素会有多困难(我的自我项目的一个要求是能够重新排序元素)每当需要时队列).
仍在努力尝试将这4个查询结合起来.我得到了这个:
MATCH (theCard:Card) WHERE ID(theCard)=22 OPTIONAL MATCH (before:Card)-[btc:NEXT_CARD]->(theCard:Card)-[tca:NEXT_CARD]->(after:Card) OPTIONAL MATCH (listOfOne:List)-[lootc:TAIL_CARD]->(theCard:Card)<-[tcloo:HEAD_CARD]-(listOfOne:List) OPTIONAL MATCH (listToHead:List)-[lthtc:HEAD_CARD]->(theCard:Card)-[tcs:NEXT_CARD]->(second:Card) OPTIONAL MATCH (listToTail:List)-[ltttc:TAIL_CARD]->(theCard:Card)-[tcntl:PREV_CARD]->(nextToLast:Card) RETURN theCard, before, btc, tca, after, listOfOne, lootc, tcloo, listToHead, lthtc, tcs, second, listToTail, ltttc, tcntl, nextToLast
当找不到某些东西时返回NULL,找到某些东西时返回节点/关系.我认为这可能是一个很好的起点,所以我添加了以下内容:
MATCH (theCard:Card) WHERE ID(theCard)=22 OPTIONAL MATCH (before:Card)-[btc:NEXT_CARD]->(theCard:Card)-[tca:NEXT_CARD]->(after:Card) OPTIONAL MATCH (listOfOne:List)-[lootc:TAIL_CARD]->(theCard:Card)<-[tcloo:HEAD_CARD]-(listOfOne:List) OPTIONAL MATCH (listToHead:List)-[lthtc:HEAD_CARD]->(theCard:Card)-[tcs:NEXT_CARD]->(second:Card) OPTIONAL MATCH (listToTail:List)-[ltttc:TAIL_CARD]->(theCard:Card)-[tcntl:PREV_CARD]->(nextToLast:Card) WITH theCard, CASE WHEN before IS NULL THEN [] ELSE COLLECT(before) END AS beforeList, before, btc, tca, after, listOfOne, lootc, tcloo, listToHead, lthtc, tcs, second, listToTail, ltttc, tcntl, nextToLast FOREACH (value IN beforeList | CREATE (before)-[:NEXT_CARD]->(after)) FOREACH (value IN beforeList | CREATE (after)-[:PREV_CARD]->(before)) FOREACH (value IN beforeList | DELETE btc) FOREACH (value IN beforeList | DELETE tca) RETURN theCard
当我执行此操作时(选择了ID before=NULL
,我的笔记本电脑的粉丝开始疯狂地旋转,查询永远不会返回,最终neo4j浏览器说它已经失去了与服务器的连接.结束查询的唯一方法是停止服务器.
所以我将查询更改为更简单:
MATCH (theCard:Card) WHERE ID(theCard)=22 OPTIONAL MATCH (before:Card)-[btc:NEXT_CARD]->(theCard:Card)-[tca:NEXT_CARD]->(after:Card) OPTIONAL MATCH (listOfOne:List)-[lootc:TAIL_CARD]->(theCard:Card)<-[tcloo:HEAD_CARD]-(listOfOne:List) OPTIONAL MATCH (listToHead:List)-[lthtc:HEAD_CARD]->(theCard:Card)-[tcs:NEXT_CARD]->(second:Card) OPTIONAL MATCH (listToTail:List)-[ltttc:TAIL_CARD]->(theCard:Card)-[tcntl:PREV_CARD]->(nextToLast:Card) RETURN theCard, CASE WHEN before IS NULL THEN [] ELSE COLLECT(before) END AS beforeList, before, btc, tca, after, listOfOne, lootc, tcloo, listToHead, lthtc, tcs, second, listToTail, ltttc, tcntl, nextToLast
而且我仍然在一个无限循环或其他东西......所以我想这条线CASE WHEN before IS NULL THEN [] ELSE COLLECT(before) END AS beforeList
不是一个好主意...有关如何从这里开始的任何建议?我走错了路吗?
最后,经过大量研究,我发现了一种编写单个查询的方法,可以处理所有可能的场景.我不知道这是否是实现我想要实现的目标的最佳方式,但它看起来优雅而紧凑.你怎么看?
// first let's get a hold of the card we want to archive MATCH (theCard:Card) WHERE ID(theCard)=44 // next, let's get a hold of the correspondent archive list node, since we need to move the card in that list OPTIONAL MATCH (theCard)<-[:NEXT_CARD|HEAD_CARD*]-(theList:List)<-[:NEXT_LIST|HEAD_LIST*]-(theProject:Project)-[:ARCHIVE_LIST]->(theArchive:List) // let's check if we are in the case where the card to be archived is in the middle of a list OPTIONAL MATCH (before:Card)-[btc:NEXT_CARD]->(theCard:Card)-[tca:NEXT_CARD]->(after:Card) OPTIONAL MATCH (next:Card)-[ntc:PREV_CARD]->(theCard:Card)-[tcp:PREV_CARD]->(previous:Card) // let's check if the card to be archived is the only card in the list OPTIONAL MATCH (listOfOne:List)-[lootc:TAIL_CARD]->(theCard:Card)<-[tcloo:HEAD_CARD]-(listOfOne:List) // let's check if the card to be archived is at the head of the list OPTIONAL MATCH (listToHead:List)-[lthtc:HEAD_CARD]->(theCard:Card)-[tcs:NEXT_CARD]->(second:Card)-[stc:PREV_CARD]->(theCard:Card) // let's check if the card to be archived is at the tail of the list OPTIONAL MATCH (listToTail:List)-[ltttc:TAIL_CARD]->(theCard:Card)-[tcntl:PREV_CARD]->(nextToLast:Card)-[ntltc:NEXT_CARD]->(theCard:Card) WITH theCard, theList, theProject, theArchive, CASE WHEN theArchive IS NULL THEN [] ELSE [(theArchive)] END AS archives, CASE WHEN before IS NULL THEN [] ELSE [(before)] END AS befores, before, btc, tca, after, CASE WHEN next IS NULL THEN [] ELSE [(next)] END AS nexts, next, ntc, tcp, previous, CASE WHEN listOfOne IS NULL THEN [] ELSE [(listOfOne)] END AS listsOfOne, listOfOne, lootc, tcloo, CASE WHEN listToHead IS NULL THEN [] ELSE [(listToHead)] END AS listsToHead, listToHead, lthtc, tcs, second, stc, CASE WHEN listToTail IS NULL THEN [] ELSE [(listToTail)] END AS listsToTail, listToTail, ltttc, tcntl, nextToLast, ntltc // let's handle the case in which the archived card was in the middle of a list FOREACH (value IN befores | CREATE (before)-[:NEXT_CARD]->(after) CREATE (after)-[:PREV_CARD]->(before) DELETE btc, tca) FOREACH (value IN nexts | DELETE ntc, tcp) // let's handle the case in which the archived card was the one and only card in the list FOREACH (value IN listsOfOne | CREATE (listOfOne)-[:HEAD_CARD]->(listOfOne) CREATE (listOfOne)-[:TAIL_CARD]->(listOfOne) DELETE lootc, tcloo) // let's handle the case in which the archived card was at the head of the list FOREACH (value IN listsToHead | CREATE (listToHead)-[:HEAD_CARD]->(second) DELETE lthtc, tcs, stc) // let's handle the case in which the archived card was at the tail of the list FOREACH (value IN listsToTail | CREATE (listToTail)-[:TAIL_CARD]->(nextToLast) DELETE ltttc, tcntl, ntltc) // finally, let's move the card in the archive // first get a hold of the archive list to which we want to add the card WITH theCard, theArchive // first get a hold of the list to which we want to add the new card OPTIONAL MATCH (theArchive)-[tact:TAIL_CARD]->(currentTail:Card) // check if the list is empty OPTIONAL MATCH (theArchive)-[tata1:TAIL_CARD]->(theArchive)-[tata2:HEAD_CARD]->(theArchive) WITH theArchive, theCard, CASE WHEN currentTail IS NULL THEN [] ELSE [(currentTail)] END AS currentTails, currentTail, tact, CASE WHEN tata1 IS NULL THEN [] ELSE [(theArchive)] END AS emptyLists, tata1, tata2 // handle the case in which the list already had at least one card FOREACH (value IN currentTails | CREATE (theArchive)-[:TAIL_CARD]->(theCard) CREATE (theCard)-[:PREV_CARD]->(currentTail) CREATE (currentTail)-[:NEXT_CARD]->(theCard) DELETE tact) // handle the case in which the list was empty FOREACH (value IN emptyLists | CREATE (theArchive)-[:TAIL_CARD]->(theCard) CREATE (theArchive)-[:HEAD_CARD]->(theCard) DELETE tata1, tata2) RETURN theCard
根据Wes的建议,我决定改变应用程序中每个队列的处理方式,添加两个额外的节点,即头部和尾部.
将头部和尾部的概念从简单的关系移动到节点允许在插入新卡时具有单个案例.即使在空队列的特殊情况下......
我们要做的就是在队列的尾部添加一张新卡:
找到[PREV_CARD]连接的(上一个)节点和[NEXT_CARD]关系到队列的(尾部)节点
创建一个(newCard)节点
使用[PREV_CARD]和[NEXT_CARD]关系将(newCard)节点连接到(tail)节点
使用[PREV_CARD]和[NEXT_CARD]关系将(newCard)节点连接到(上一个)节点
最后删除将(上一个)节点连接到队列的(尾部)节点的原始[PREV_CARD]和[NEXT_CARD]关系
转换为以下cypher查询:
MATCH (theList:List)-[tlt:TAIL_CARD]->(tail)-[tp:PREV_CARD]->(previous)-[pt:NEXT_CARD]->(tail) WHERE ID(theList)={{listId}} WITH theList, tail, tp, pt, previous CREATE (newCard:Card { title: "Card Title", description: "" }) CREATE (tail)-[:PREV_CARD]->(newCard)-[:NEXT_CARD]->(tail) CREATE (newCard)-[:PREV_CARD]->(previous)-[:NEXT_CARD]->(newCard) DELETE tp,pt RETURN newCard
现在让我们重新考虑我们想要存档卡的用例.我们来看看架构:
我们有:
每个项目都有一个列表队列
每个项目都有一个存档队列来存储所有存档的卡
每个列表都有一个卡队列
在之前的队列架构中,我有4种不同的场景,具体取决于要归档的卡是头部,尾部还是介于两者之间的卡,或者它是否是在Quee中留下的最后一张卡.
现在,通过引入头部和尾部节点,只有一种情况,因为即使在队列为空的情况下,头部和尾部节点仍然存在:
我们需要在(theCard)节点之前和之后找到(上一个)和(下一个)节点,该节点是我们要归档的节点
然后,我们需要连接(上一个)和(下一个)[NEXT_CARD]和[PREV_CARD]关系
然后,我们需要删除所有连接(theCard)与(上一个)和(下一个)节点的关系
生成的密码查询可以细分为三个不同的部分.第一部分负责找到(theArchive)节点,给定(theCard)节点的ID:
MATCH (theCard)<-[:NEXT_CARD|HEAD_CARD*]-(l:List)<-[:NEXT_LIST*]-(h)<-[:HEAD_LIST]-(p:Project)-[:ARCHIVE]->(theArchive:Archive) WHERE ID(theCard)={{cardId}}
接下来,我们执行前面几行描述的逻辑:
WITH theCard, theArchive MATCH (previous)-[ptc:NEXT_CARD]->(theCard)-[tcn:NEXT_CARD]->(next)-[ntc:PREV_CARD]->(theCard)-[tcp:PREV_CARD]->(previous) WITH theCard, theArchive, previous, next, ptc, tcn, ntc, tcp CREATE (previous)-[:NEXT_CARD]->(next)-[:PREV_CARD]->(previous) DELETE ptc, tcn, ntc, tcp
最后,我们在存档队列的尾部插入(theCard):
WITH theCard, theArchive MATCH (theArchive)-[tat:TAIL_CARD]->(archiveTail)-[tp:PREV_CARD]->(archivePrevious)-[pt:NEXT_CARD]->(archiveTail) WITH theCard, theArchive, archiveTail, tp, pt, archivePrevious CREATE (archiveTail)-[:PREV_CARD]->(theCard)-[:NEXT_CARD]->(archiveTail) CREATE (theCard)-[:PREV_CARD]->(archivePrevious)-[:NEXT_CARD]->(theCard) DELETE tp,pt RETURN theCard
我希望你发现这最后一个编辑很有趣,因为我找到了这个练习.我想再次感谢Wes在这个有趣的(至少对我来说)实验中的远程帮助(通过Twitter和Stack Overflow).