知识库

条件式 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 的相同限制。

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

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

这对于条件式 Cypher 可能是一个问题,因为根据定义,您正在评估一个条件作为过滤器,以确定是否执行某项操作。

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

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

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

当没有其他非聚合变量作为分组键时,聚合(例如 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 是针对给定列表中的每个元素执行的特性。如果列表有一个元素,则 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 执行只读 ifQueryelseQuery

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

根据条件,使用给定的 params 执行写入 ifQueryelseQuery

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

params 映射是调用的最后一个参数:{bruce:bruceWayne},它允许 bruceWayne 变量以 bruce 的形式对任何条件查询可见。如果需要,可以向 params 映射添加更多参数。

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

目前,当执行非空条件查询且查询不返回任何内容时,不会为行生成任何 YIELD,从而清除该行。对于原始行,CALL 之后的任何内容现在都是无操作,因为不再有行可供执行(Cypher 操作按行执行)。

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

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

为了避免混淆,最好在所有条件查询中始终包含 RETURN(除了您完全留空的那些,例如无操作的 else 查询……它们按预期行为)。

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

© . All rights reserved.