知识库

条件语句下的 Cypher 执行

在某些情况下,您将编写需要一些条件逻辑的 Cypher 查询,您希望根据情况执行不同的 Cypher 语句。

目前,Cypher 不包含用于解决这种情况的原生条件功能,但有一些可以使用的工作区。

本文介绍了执行条件 Cypher 语句的方法。

关于 CASE 的说明

CASE 表达式执行一些条件逻辑,但逻辑只能用于输出表达式。它不能用于有条件地执行 Cypher 子句。

在 4.1+ 中使用关联子查询

Neo4j 4.1 引入了关联子查询,让我们可以使用中间查询中存在的变量执行子查询。通过结合子查询的使用和过滤,我们可以使用子查询来实现条件 Cypher 执行。

这要求在子查询 CALL 块中使用 WITH 作为第一个子句,目的是将变量导入到子查询中。

此导入使用有一些特殊的限制,这些限制通常不适用于 WITH 的使用。

  1. 您只能包含来自外部查询的变量,不能包含其他任何变量。您不能在初始 WITH 中执行计算、聚合或引入新变量。

  2. 您不能在初始 WITH 中对任何变量进行别名。

  3. 您不能在初始 WITH 之后使用 WHERE 子句进行过滤。

如果您尝试执行任何这些操作,您将会遇到某种错误,例如

Importing WITH should consist only of simple references to outside variables. Aliasing or expressions are not supported.

或者更隐蔽的是,如果您尝试在初始 WITH 之后使用 WHERE 子句

Variable `x` not defined

(其中变量是 WITH 子句中出现的第一个变量)

您可以通过简单地在用于导入到子查询的初始 WITH 之后引入另一个 WITH 子句来绕过所有这些限制,如下所示

MATCH (bruce:Person {name:'Bruce Wayne'})
CALL {
    WITH bruce
    WITH bruce
    WHERE bruce.isOrphan
    MERGE (batman:Hero {name:'Batman'})
    CREATE (bruce)-[:SuperheroPersona]->(batman)
    WITH count(batman) as count
    RETURN count = 1 as isBatman
}
RETURN isBatman

这演示了通过添加第二个 WITH 子句来过滤导入到子查询的变量的能力,这个 WITH 子句没有与用于导入到子查询的初始 WITH 相同的限制。

子查询必须返回一行,以便外部查询继续执行

子查询不独立于外部查询,如果它们不生成任何行,则外部查询将没有行来继续执行。

这可能是条件 Cypher 出现问题的原因,因为根据定义,您正在评估条件作为过滤器,以确定是否执行某些操作。

如果该条件评估为假,则该行将被清除,这通常在子查询本身中是可以的(如果您不想在布鲁斯还不是孤儿的时候创建蝙蝠侠),但您通常希望无论子查询中发生了什么,都继续执行,并且可能返回一个布尔值,表示条件是否成功。

有一些方法可以避免行被清除。

在子查询返回之前使用独立聚合来恢复一行

聚合(例如 count()),当没有其他非聚合变量作为分组键存在时,即使该行已被清除,也可以恢复该行。

这是因为获取 0 行的 count() 或对 0 行进行 collect() 以生成一个空集合是有效的。

同样,当您执行此聚合时,必须不存在其他非聚合变量。

在上面的示例中,我们在子查询中使用了这种技术,以便外部查询无论条件如何评估都可以继续执行。

    WITH count(batman) as count
    RETURN count = 1 as isBatman

使用该 count(),无论查询如何评估,我们将获得 0 或 1,这使得我们可以在子查询完成后继续执行。

使用 UNION 子查询来涵盖所有可能的条件

我们可以在子查询中使用 UNION,其中所有联合查询的集合涵盖所有可能的条件结果。这确保将存在一条成功的执行路径,并返回一行,使外部查询能够继续执行。

这对于将 if/else 或 case 逻辑保持在一起也很有用,否则您将不得不为每个条件块使用单独的子查询。

使用这种方法,您不再需要使用聚合来确保行保持不变,您只需要确保无论如何,至少有一个 UNIONed 查询会成功即可。

MATCH (bruce:Person {name:'Bruce Wayne'})
CALL {
    WITH bruce
    WITH bruce
    WHERE bruce.isOrphan
    MERGE (batman:Hero {name:'Batman'})
    CREATE (bruce)-[:SuperheroPersona]->(batman)
    RETURN true as isBatman

    UNION

    WITH bruce
    WITH bruce
    WHERE NOT coalesce(bruce.isOrphan, false)
    SET bruce.name = 'Bruce NOT BATMAN Wayne'
    RETURN false as isBatman
}
RETURN isBatman

请注意,我们必须为每个 UNIONed 查询使用导入 WITH,以确保它们都从外部查询中导入变量,并且我们仍然必须使用第二个 WITH 来允许我们进行过滤。

由于对可以联合在一起的查询数量没有限制,因此可以使用这种方法处理多个条件评估。

使用 FOREACH 进行只写 Cypher

FOREACH 子句可用于执行等效于 IF 条件的操作,其限制是只能使用写入子句(MERGE、CREATE、DELETE、SET、REMOVE)。

这依赖于 FOREACH 子句中 Cypher 针对给定列表中的每个元素执行的特性。如果列表包含 1 个元素,则 FOREACH 中的 Cypher 将执行。如果列表为空,则包含的 Cypher 将不会执行。

我们可以使用 CASE 评估布尔条件并输出一个单元素列表或一个空列表,这将驱动条件 Cypher 执行(执行后续的只写子句或不执行)。

例如

MATCH (node:Node {id:12345})
FOREACH (i in CASE WHEN node.needsUpdate THEN [1] ELSE [] END |
  SET node.newProperty = 5678
  REMOVE node.needsUpdate
  SET node:Updated)
...

要获得 if/else 逻辑的等效结果,必须为 else 部分使用单独的 FOREACH。

请记住,任何其他非写入子句(例如 MATCH、WITH 和 CALL)都无法使用这种方法。

APOC 条件过程

或者,APOC 过程库包含专门用于 条件 Cypher 执行 的过程。

过程有两种类型

apoc.when() - 当您只有 if(可能还有 else)查询要根据单个条件执行时。不能写入图。

apoc.case() - 当您想要检查一系列独立条件时,每个条件都有自己的单独 Cypher 查询,如果条件为真,则执行该查询。只有第一个评估为真的条件会执行其关联的查询。如果没有条件为真,则可以提供 else 查询作为默认值。不能写入图。

读写变体

上面显示的过程仅具有读取权限,不允许写入图,因此如果条件 Cypher 中有任何写入操作,查询将出错。

有一些变体可以写入图

apoc.do.when() - 与 apoc.when() 相似的条件 if/else Cypher 执行,但允许写入图。

apoc.do.case() - 与 apoc.case() 相似的条件 case Cypher 执行,但允许写入图。

这是必要的,因为过程的读写模式必须在过程代码中声明。

仅具有只读过程将无法写入图。

仅具有写入功能的过程意味着它无法被只读用户调用,即使条件 Cypher 未执行任何写入操作。

这两种方法都是必要的,以便无论用户类型或条件 Cypher 查询的需求如何,都能提供完整的功能。

完整的签名

CALL apoc.when(condition, ifQuery, elseQuery:'', params:{}) yield value

根据条件,使用给定的 params 执行只读 ifQuery 或 elseQuery。

CALL apoc.do.when(condition, ifQuery, elseQuery:'', params:{}) yield value

根据条件,使用给定的 params 执行写入 ifQuery 或 elseQuery。

CALL apoc.case([condition, query, condition, query, …​], elseQuery:'', params:{}) yield value

给定一个条件/只读查询对列表,执行第一个条件为真(或如果都不为真则执行elseQuery)的关联查询,并使用给定的params

CALL apoc.do.case([condition, query, condition, query, …​], elseQuery:'', params:{}) yield value

给定一个条件/写入查询对列表,执行第一个条件为真(或如果都不为真则执行elseQuery)的关联查询,并使用给定的params

在所有情况下,condition必须是一个布尔表达式,所有条件查询(ifQueryelseQueryquery)实际上都是Cypher查询字符串,并且必须用引号括起来。

因此,请注意在查询字符串中正确处理引号。如果查询字符串本身在双引号中,则该查询中的所有字符串都应使用单引号(反之亦然)。

使用这些过程可能很棘手。以下是一些更实用的技巧,可以帮助您避免最常见的陷阱。

处理复杂嵌套查询中的引号/转义符

对于更复杂的查询(例如必须在多个级别处理引号的嵌套查询),请考虑先将查询字符串定义为一个变量,然后将该变量传递给过程,或者将条件查询作为参数传递给查询本身。这可能会使您免受处理Java字符串中转义字符的困扰。

传递必须在条件查询中可见的参数

执行时,条件Cypher查询无法看到CALL外部的变量。

如果查询必须查看或使用变量,请将其作为params映射参数的一部分传递给调用,如下所示

MATCH (bruceWayne:Person {name:'Bruce Wayne'})
CALL apoc.do.when(bruceWayne.isOrphan, "MERGE (batman:Hero {name:'Batman'}) CREATE (bruce)-[:SuperheroPersona]->(batman) RETURN bruce", "SET bruce.name = 'Bruce NOT BATMAN Wayne' RETURN bruce", {bruce:bruceWayne}) YIELD value
...

参数映射是调用的最后一个参数:{bruce:bruceWayne},它允许所有条件查询将bruceWayne变量作为bruce访问。如果需要,可以将其他参数添加到参数映射中。

如果要在CALL后继续执行查询,则条件查询必须返回一些内容

目前,当执行(非空)条件查询并且查询没有返回任何内容时,该行不会产生任何YIELD结果,从而擦除该行。对于原始行,CALL后的任何操作现在都将变成无操作,因为不再有要执行的行(Cypher操作按行执行)。

虽然这对于条件CALL是查询的最后一部分(因此CALL后没有更多要执行的操作)时可能没问题,但对于想要继续查询但忘记在条件查询中添加RETURN的任何人来说,这种行为将是令人不快和困惑的意外。

由此产生的症状是查询会执行到条件CALL,但(可能对于所有行,也可能仅对于子集)CALL后的查询部分不会执行。

为了避免混淆,可能有助于始终在所有条件查询中包含RETURN(除了您完全留空的查询,例如无操作的else查询……它们的执行结果与预期一致)。

这种经常令人困惑的行为将在2020年之后的APOC更新中修复。