如何在 Neo4J 中处理队列?

Posted

技术标签:

【中文标题】如何在 Neo4J 中处理队列?【英文标题】:How to handle a queue in Neo4J? 【发布时间】:2014-01-14 13:30:50 【问题描述】:

在我的 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

当要归档的节点是队列的head

// 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

当要归档的节点是队列的tail

// 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

我已尝试使用 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 的建议,我决定更改应用程序中每个队列的处理方式,添加两个额外节点,headtail

插入新卡

headtail 的概念从简单的关系转移到节点允许在插入新卡时有一个 case。即使在空队列的特殊情况下......

将一张新卡添加到队列尾部所需要做的就是:

找到通过 [PREV_CARD] 和 [NEXT_CARD] 关系连接到队列的(尾)节点的(前一个)节点 创建一个(newCard)节点 使用 [PREV_CARD] 和 [NEXT_CARD] 关系将 (newCard) 节点连接到 (tail) 节点 使用 [PREV_CARD] 和 [NEXT_CARD] 关系将 (newCard) 节点连接到 (previous) 节点 最后删除原来的[PREV_CARD]和一个[NEXT_CARD]关系将(前一个)节点连接到队列的(尾)节点

翻译成以下密码查询:

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 种不同的场景,取决于要归档的卡片是头、尾还是中间的卡片,或者是队列中剩下的最后一张卡片。

现在,随着 headtail 节点的引入,只有一种情况,因为 head 和 tail 节点仍然存在,即使在队列为空的情况:

我们需要在 (theCard) 节点之前和之后找到 (previous) 和 (next) 节点,这是我们要归档的节点 然后,我们需要用 [NEXT_CARD] 和 [PREV_CARD] 关系连接(上一个)和(下一个) 然后,我们需要删除将 (theCard) 连接到 (previous) 和 (next) 节点的所有关系

生成的密码查询可以细分为三个不同的部分。第一部分负责查找(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)。

【问题讨论】:

您可能应该使用一些真实数据从您的帖子中创建一个图表要点,以便每个人都可以看到数据是如何从您的查询中返回的?而且我们还有一些示例数据可供使用/测试。 您可能有兴趣查看我的跳过列表图要点...它通过永不删除的尾部和头部来处理空列表,因此情况始终相同(删除内部节点) :gist.neo4j.org/?8112746 @WesFreeman 非常有趣。所以你把头和尾的概念从关系中,就像我做的那样,转移到节点中。这确实让事情变得简单多了!非常聪明! 【参考方案1】:

您的解决方案似乎足够好,也符合您的目的。我只是给你补充一些建议..那..当你检查要归档的卡在列表中间并确认它在中间时,你不需要检查它是否在头部或列表的尾部,反之亦然。

【讨论】:

【参考方案2】:

一个很好的问题——图中的双向链表。我最近研究了一个类似的概念,我需要跟踪一个列表,但试图想出一种方法来避免需要知道如何处理列表末尾边缘情况的困难。我努力的结果是this graph gist about skip lists in cypher.

转换为双向链表,如下所示:

CREATE
 (head:Head), 
 (tail:Tail),
 (head)-[:NEXT]->(tail),
 (tail)-[:PREV]->(head),
 (a:Archive)-[:NEXT]->(:ArchiveTail)-[:PREV]->(a);
; // we need something to start at, and an archive list

然后你可以用简单的队列节点:

MATCH (tail:Tail)-[p:PREV]->(prev),
      (tail)<-[n:NEXT]-(prev)
CREATE (new text:"new card")<-[:NEXT]-(prev),  
       (new)-[:PREV]->(prev),
       (new)<-[:PREV]-(tail),
       (new)-[:NEXT]->(tail) 
DELETE p, n

和归档节点用的相当简单:

MATCH (toArchive)-[nn:NEXT]->(next),
      (toArchive)<-[np:PREV]-(next),
      (toArchive)<-[pn:NEXT]-(prev),
      (toArchive)-[pp:PREV]->(prev) 
WHERE toArchive.text = "new card 2" 
CREATE (prev)-[:NEXT]->(next)-[:PREV]->(prev) 
DELETE nn, np, pn, pp 
WITH toArchive 
MATCH (archive:Archive)-[n:NEXT]->(first)-[p:PREV]->(archive) 
CREATE (archive)-[:NEXT]->(toArchive)<-[:PREV]-(first),
       (archive)<-[:PREV]-(toArchive)-[:NEXT]->(first) 
DELETE n, p

在算法上,您的用例实际上比跳过列表容易得多,因为您可以通过将尾部直接将卡片排队到末尾来避免大多数对 varlength 路径的需求。希望其他实施类似想法的人会发现您的 SO 问题很有用。

【讨论】:

以上是关于如何在 Neo4J 中处理队列?的主要内容,如果未能解决你的问题,请参考以下文章

如何在java程序中引入neo4j数据库

如何在 Neo4j 中删除/创建数据库?

如何将neo4j图数据库显示的图在自己的应用中展示

在 neo4j 中,如何按日期索引并在日期范围内搜索?

如何在 Neo4j 中根据日期顺序创建关系

如何在 Neo4j 中隐藏节点之间不需要的关系