自定义查询

Spring Data Neo4j,与其他 Spring Data 模块一样,允许您在仓库中指定自定义查询。当您无法通过派生查询函数表达查找逻辑时,自定义查询会非常有用。

因为 Spring Data Neo4j 在底层大量采用记录导向的方式工作,务必记住这一点,并且不要为同一个“根节点”构建包含多个记录的结果集。

请也查看常见问题 (FAQ) 以了解从仓库中使用自定义查询的其他形式,特别是如何结合自定义映射使用自定义查询:自定义查询与自定义映射

带关系查询

警惕笛卡尔积

假设您有一个查询,例如 MATCH (m:Movie{title: 'The Matrix'})←[r:ACTED_IN]-(p:Person) return m,r,p,其结果类似这样:

多条记录(已简化)
+------------------------------------------------------------------------------------------+
| m        | r                                    | p                                      |
+------------------------------------------------------------------------------------------+
| (:Movie) | [:ACTED_IN {roles: ["Emil"]}]        | (:Person {name: "Emil Eifrem"})        |
| (:Movie) | [:ACTED_IN {roles: ["Agent Smith"]}] | (:Person {name: "Hugo Weaving})        |
| (:Movie) | [:ACTED_IN {roles: ["Morpheus"]}]    | (:Person {name: "Laurence Fishburne"}) |
| (:Movie) | [:ACTED_IN {roles: ["Trinity"]}]     | (:Person {name: "Carrie-Anne Moss"})   |
| (:Movie) | [:ACTED_IN {roles: ["Neo"]}]         | (:Person {name: "Keanu Reeves"})       |
+------------------------------------------------------------------------------------------+

映射的结果很可能无法使用。如果这被映射到一个列表中,则会包含电影的重复项,但这部电影只会有一个关系。

每个根节点获取一条记录

要获取正确的对象,需要在查询中 collect(收集)关系和相关节点:MATCH (m:Movie{title: 'The Matrix'})←[r:ACTED_IN]-(p:Person) return m,collect(r),collect(p)

单条记录(已简化)
+------------------------------------------------------------------------+
| m        | collect(r)                     | collect(p)                 |
+------------------------------------------------------------------------+
| (:Movie) | [[:ACTED_IN], [:ACTED_IN], ...]| [(:Person), (:Person),...] |
+------------------------------------------------------------------------+

通过将此结果作为单条记录,Spring Data Neo4j 可以将所有相关节点正确地添加到根节点。

深入图

上面的示例假设您只尝试获取第一级相关节点。有时这不足够,图中有更深层的节点也应作为映射实例的一部分。有两种方法可以实现这一点:数据库端规约或客户端规约。

为此,上面的示例中,通过初始电影返回的人员也应包含他们参演的电影。

image$movie graph deep
图 1. 《黑客帝国》和基努·里维斯的示例

数据库端规约

请记住,Spring Data Neo4j 只能正确处理基于记录的结果,一个实体实例的结果需要位于一条记录中。使用 Cypher 的 path(路径)功能是获取图中所有分支的有效选项。

朴素的基于路径的方法
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN p;

这将导致多条路径未合并到一条记录中。可以调用 collect(p),但 Spring Data Neo4j 在映射过程中不理解路径的概念。因此,需要从结果中提取节点和关系。

提取节点和关系
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN m, nodes(p), relationships(p);

由于存在多条从《黑客帝国》到另一部电影的路径,结果仍然不是单条记录。这就是 Cypher 的 reduce 函数发挥作用的地方。

规约节点和关系
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
WITH collect(p) as paths, m
WITH m,
reduce(a=[], node in reduce(b=[], c in [aa in paths | nodes(aa)] | b + c) | case when node in a then a else a + node end) as nodes,
reduce(d=[], relationship in reduce(e=[], f in [dd in paths | relationships(dd)] | e + f) | case when relationship in d then d else d + relationship end) as relationships
RETURN m, relationships, nodes;

reduce 函数允许我们扁平化来自各种路径的节点和关系。结果我们将得到一个类似于 每个根节点获取一条记录 的元组,但在集合中混合了关系类型或节点。

客户端规约

如果规约应发生在客户端,Spring Data Neo4j 允许您映射列表的列表,这些列表可以是关系或节点。尽管如此,仍然适用要求,即返回的记录应包含所有信息,以正确地填充结果实体实例。

从路径收集节点和关系
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN m, collect(nodes(p)), collect(relationships(p));

附加的 collect 语句创建以下格式的列表:

[[rel1, rel2], [rel3, rel4]]

这些列表现在将在映射过程中被转换为扁平列表。

决定是采用客户端规约还是数据库端规约取决于将生成的数据量。当使用 reduce 函数时,所有路径都需要首先在数据库内存中创建。另一方面,大量需要在客户端合并的数据会导致客户端更高的内存使用。

使用路径填充并返回实体列表

假设图结构如下:

image$custom query.paths
图 2. 带有出站关系的图

以及映射中显示的领域模型(为简洁起见,已省略构造函数和访问器):

带有出站关系的图的领域模型。
@Node
public class SomeEntity {

    @Id
    private final Long number;

    private String name;

    @Relationship(type = "SOME_RELATION_TO", direction = Relationship.Direction.OUTGOING)
    private Set<SomeRelation> someRelationsOut = new HashSet<>();
}

@RelationshipProperties
public class SomeRelation {

    @RelationshipId
    private Long id;

    private String someData;

    @TargetNode
    private SomeEntity targetPerson;
}

如您所见,关系仅是出站的。生成的查找方法(包括 findById)总是尝试匹配要映射的根节点。从那里开始,所有相关对象都将被映射。在应该只返回一个对象的查询中,将返回该根对象。在返回多个对象的查询中,将返回所有匹配的对象。当然,从这些返回的对象中出站和入站的关系都会被填充。

假设以下 Cypher 查询:

MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
RETURN leaf, collect(nodes(p)), collect(relationships(p))

它遵循每个根节点获取一条记录的建议,并且对于您此处要匹配的叶节点非常有效。然而:这仅适用于返回 0 或 1 个映射对象的所有场景。虽然该查询将像以前一样填充所有关系,但它不会返回所有 4 个对象。

可以通过返回整个路径来改变这一点:

MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
RETURN p

这里我们确实想利用路径 p 实际上返回 3 行包含到所有 4 个节点的路径这一事实。所有 4 个节点都将被填充、链接在一起并返回。

自定义查询中的参数

您可以在 Neo4j Browser 或 Cypher-Shell 中使用标准的 Cypher 查询完全相同地进行操作,使用 $ 语法(从 Neo4j 4.0 及更高版本开始,旧的用于 Cypher 参数的 ${foo} 语法已从数据库中移除)。

ARepository.java
public interface ARepository extends Neo4jRepository<AnAggregateRoot, String> {

	@Query("MATCH (a:AnAggregateRoot {name: $name}) RETURN a") (1)
	Optional<AnAggregateRoot> findByCustomQuery(String name);
}
1 这里我们通过参数名称来引用它。您也可以使用 $0 等替代。
您需要使用 -parameters 参数编译 Java 8+ 项目,以便命名参数无需额外注解即可工作。Spring Boot 的 Maven 和 Gradle 插件会自动为您执行此操作。如果由于某种原因这不可行,您可以添加 @Param 并显式指定名称,或使用参数索引。

标记为 @Node 的映射实体(所有带有 @Node 的内容)作为参数传递给带有自定义查询注解的函数时,将被转换为嵌套的 map。以下示例展示了作为 Neo4j 参数的结构。

假设 Movie, VertexActor 类如 电影模型 中所示进行注解。

“标准”电影模型
@Node
public final class Movie {

    @Id
    private final String title;

    @Property("tagline")
    private final String description;

    @Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
    private final List<Actor> actors;

    @Relationship(value = "DIRECTED", direction = Direction.INCOMING)
    private final List<Person> directors;
}

@Node
public final class Person {

    @Id @GeneratedValue
    private final Long id;

    private final String name;

    private Integer born;

    @Relationship("REVIEWED")
    private List<Movie> reviewed = new ArrayList<>();
}

@RelationshipProperties
public final class Actor {

	@RelationshipId
	private final Long id;

    @TargetNode
    private final Person person;

    private final List<String> roles;
}

interface MovieRepository extends Neo4jRepository<Movie, String> {

    @Query("MATCH (m:Movie {title: $movie.__id__})\n"
           + "MATCH (m) <- [r:DIRECTED|REVIEWED|ACTED_IN] - (p:Person)\n"
           + "return m, collect(r), collect(p)")
    Movie findByMovie(@Param("movie") Movie movie);
}

将一个 Movie 实例传递给上述仓库方法,将生成以下 Neo4j map 参数:

{
  "movie": {
    "__labels__": [
      "Movie"
    ],
    "__id__": "The Da Vinci Code",
    "__properties__": {
      "ACTED_IN": [
        {
          "__properties__": {
            "roles": [
              "Sophie Neveu"
            ]
          },
          "__target__": {
            "__labels__": [
              "Person"
            ],
            "__id__": 402,
            "__properties__": {
              "name": "Audrey Tautou",
              "born": 1976
            }
          }
        },
        {
          "__properties__": {
            "roles": [
              "Sir Leight Teabing"
            ]
          },
          "__target__": {
            "__labels__": [
              "Person"
            ],
            "__id__": 401,
            "__properties__": {
              "name": "Ian McKellen",
              "born": 1939
            }
          }
        },
        {
          "__properties__": {
            "roles": [
              "Dr. Robert Langdon"
            ]
          },
          "__target__": {
            "__labels__": [
              "Person"
            ],
            "__id__": 360,
            "__properties__": {
              "name": "Tom Hanks",
              "born": 1956
            }
          }
        },
        {
          "__properties__": {
            "roles": [
              "Silas"
            ]
          },
          "__target__": {
            "__labels__": [
              "Person"
            ],
            "__id__": 403,
            "__properties__": {
              "name": "Paul Bettany",
              "born": 1971
            }
          }
        }
      ],
      "DIRECTED": [
        {
          "__labels__": [
            "Person"
          ],
          "__id__": 404,
          "__properties__": {
            "name": "Ron Howard",
            "born": 1954
          }
        }
      ],
      "tagline": "Break The Codes",
      "released": 2006
    }
  }
}

节点由一个 map 表示。该 map 将始终包含 __id__,这是映射的 id 属性。在 __labels__ 下,所有标签(静态和动态)都将可用。所有属性以及关系类型,都将出现在这些 map 中,就像实体由 SDN 写入图数据库时那样。值将具有正确的 Cypher 类型,无需进一步转换。

所有关系都是 map 的列表。动态关系将相应地被解析。一对一关系也将被序列化为单元素列表。因此,要访问人员之间的一对一映射,您可以这样编写:$person.__properties__.BEST_FRIEND[0].__target__.__id__

如果一个实体与不同类型的其他节点具有相同类型的关系,它们都将出现在同一个列表中。如果您需要这样的映射,并且也需要使用这些自定义参数,则必须相应地展开它。一种方法是使用相关子查询(需要 Neo4j 4.1+)。

自定义查询中的值表达式

自定义查询中的 Spring Expression Language

Spring Expression Language (SpEL) 可以在自定义查询的 :#{} 内部使用。这里的冒号表示一个参数,这样的表达式应该在参数有意义的地方使用。然而,当使用我们的 literal 扩展时,您可以在标准 Cypher 不允许参数(例如标签或关系类型)的地方使用 SpEL 表达式。这是 Spring Data 定义查询中进行 SpEL 评估的文本块的标准方式。

以下示例基本上定义了与上面相同的查询,但使用 WHERE 子句以避免更多花括号:

ARepository.java
public interface ARepository extends Neo4jRepository<AnAggregateRoot, String> {

	@Query("MATCH (a:AnAggregateRoot) WHERE a.name = :#{#pt1 + #pt2} RETURN a")
	Optional<AnAggregateRoot> findByCustomQueryWithSpEL(String pt1, String pt2);
}

SpEL 块以 :#{ 开头,然后通过名称 (#pt1) 引用给定的 String 参数。不要与上面的 Cypher 语法混淆!SpEL 表达式将两个参数连接成一个单一值,最终传递给 附录/neo4j-client.adoc#neo4j-client。SpEL 块以 } 结束。

SpEL 还解决了两个额外的问题。我们提供了两个扩展,允许将 Sort 对象传递到自定义查询中。还记得 自定义查询faq.adoc#custom-queries-with-page-and-slice-examples 吗?通过 orderBy 扩展,您可以将带有动态排序的 Pageable 传递到自定义查询中:

orderBy 扩展
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;

public interface MyPersonRepository extends Neo4jRepository<Person, Long> {

    @Query(""
        + "MATCH (n:Person) WHERE n.name = $name RETURN n "
        + ":#{orderBy(#pageable)} SKIP $skip LIMIT $limit" (1)
    )
    Slice<Person> findSliceByName(String name, Pageable pageable);

    @Query(""
        + "MATCH (n:Person) WHERE n.name = $name RETURN n :#{orderBy(#sort)}" (2)
    )
    List<Person> findAllByName(String name, Sort sort);
}
1 Pageable 在 SpEL 上下文中始终具有名称 pageable
2 Sort 在 SpEL 上下文中始终具有名称 sort

Spring Expression Language 扩展

Literal 扩展

literal 扩展可用于在自定义查询中使标签或关系类型等内容“动态化”。标签和关系类型都不能在 Cypher 中参数化,因此必须以 literal 形式提供。

literal 扩展
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {

    @Query("MATCH (n:`:#{literal(#label)}`) RETURN n") (1)
    List<Inheritance.BaseClass> findByLabel(String label);
}
1 literal 扩展将被评估参数的 literal 值替换。

这里,literal 值已用于动态匹配标签。如果将 SomeLabel 作为参数传递给方法,将生成 MATCH (n:`SomeLabel`) RETURN n。已添加反引号以正确转义值。SDN 不会为您执行此操作,因为这可能并非所有情况下都符合您的需求。

列表扩展

对于多个值,可以使用 allOfanyOf,它们将生成一个用 &| 连接的所有值的列表。

列表扩展
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {

    @Query("MATCH (n:`:#{allOf(#label)}`) RETURN n")
    List<Inheritance.BaseClass> findByLabels(List<String> labels);

    @Query("MATCH (n:`:#{anyOf(#label)}`) RETURN n")
    List<Inheritance.BaseClass> findByLabels(List<String> labels);
}

引用标签

您已经知道如何将 Node 映射到领域对象

带有多个标签的 Node
@Node(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
public class BikeNode {
    @Id String id;

    String name;
}

这个节点有几个标签,在自定义查询中一直重复它们容易出错:您可能会忘记或拼写错误。我们提供以下表达式来缓解此问题:#{#staticLabels}。请注意,这个表达式不以冒号开头!您在带有 @Query 注解的仓库方法上使用它。

#{#staticLabels} 的实际应用
public interface BikeRepository extends Neo4jRepository<Bike, String> {

    @Query("MATCH (n:#{#staticLabels}) WHERE n.id = $nameOrId OR n.name = $nameOrId RETURN n")
    Optional<Bike> findByNameOrId(@Param("nameOrId") String nameOrId);
}

此查询将解析为:

MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.id = $nameOrId OR n.name = $nameOrId RETURN n

请注意我们如何为 nameOrId 使用了标准参数:在大多数情况下,无需通过添加 SpEL 表达式来使事情复杂化。

自定义查询中的属性占位符解析

Spring 的属性占位符可以在自定义查询的 ${} 内部使用。

ARepository.java
@Query("MATCH (a:AnAggregateRoot) WHERE a.name = :${foo} RETURN a")
Optional<AnAggregateRoot> findByCustomQueryWithPropertyPlaceholder();

在上面的示例中,如果属性 foo 被设置为 bar,则 ${foo} 块将被解析为 bar