跳到主要内容

自定义查询

DeepSeek V3 中英对照 Custom queries

Spring Data Neo4j,像所有其他的 Spring Data 模块一样,允许你在存储库中指定自定义查询。如果你无法通过派生的查询函数表达查找逻辑,这些自定义查询就会派上用场。

由于 Spring Data Neo4j 在底层大量依赖于记录导向的工作方式,因此务必牢记这一点,避免为同一个“根节点”构建包含多条记录的结果集。

提示

请也查看常见问题解答,了解从仓库使用自定义查询的替代形式,特别是如何使用带有自定义映射的自定义查询:自定义查询和自定义映射

带关系的查询

警惕笛卡尔积

假设你有一个查询 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"}) |
+------------------------------------------------------------------------------------------+

映射后的结果很可能无法使用。如果将其映射为一个列表,那么列表中会包含 Movie 的重复项,但该电影只会有一个关系。

每个根节点获取一条记录

为了获取正确的对象,需要在查询中收集关系和相关的节点: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 就能够将所有相关的节点正确地添加到根节点上。

深入图查询

上面的示例假设你只尝试获取第一层相关节点。这有时是不够的,图中可能存在更深层次的节点,这些节点也应该成为映射实例的一部分。有两种方法可以实现这一点:数据库端或客户端的归约处理。

为了这个目的,上面的示例还应该包含与初始 Movie 一起返回的 Persons 上的 Movies

电影图深度$movie graph deep

图 1. 《黑客帝国》和“基努·里维斯”示例

数据库端优化

需要注意的是,Spring Data Neo4j 只能正确处理基于记录的结果,因此一个实体实例的结果需要在一个记录中。使用 Cypher 的路径 功能是获取图中所有分支的有效选项。

MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN p;
cypher

这将导致在一个记录中存在多条未合并的路径。虽然可以调用 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

由于从《黑客帝国》到另一部电影存在多条路径,结果仍然不会是单一记录。这时就轮到 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;
cypher

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));
cypher

额外的 collect 语句会创建以下格式的列表:

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

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

备注

决定是选择客户端还是数据库端的数据缩减,取决于将生成的数据量。当使用 reduce 函数时,所有路径都需要首先在数据库的内存中创建。另一方面,如果需要在客户端合并大量数据,则会导致客户端内存使用增加。

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

给定的图形如下所示:

image$自定义查询路径

图 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;
}
java

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

假设以下 Cypher 查询:

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

它遵循了为每个根节点获取一条记录中的建议,并且对于你想要匹配的叶节点,它工作得非常好。然而:这仅在所有返回 0 或 1 个映射对象的场景中成立。虽然该查询会像之前一样填充所有关系,但它不会返回所有 4 个对象。

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

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

在这里,我们确实想要利用路径 p 实际上返回了 3 行路径,指向所有 4 个节点的事实。所有 4 个节点都将被填充、相互链接并返回。

自定义查询中的参数

你在 Neo4j 浏览器或 Cypher-Shell 中执行标准 Cypher 查询的方式与此完全相同,使用 $ 语法(从 Neo4j 4.0 开始,旧的 ${foo} 语法用于 Cypher 参数的功能已在数据库中移除)。

public interface ARepository extends Neo4jRepository<AnAggregateRoot, String> {

@Query("MATCH (a:AnAggregateRoot {name: $name}) RETURN a") 1
Optional<AnAggregateRoot> findByCustomQuery(String name);
}
java
  • 这里我们通过名称引用参数。你也可以使用 $0 等来代替。

备注

你需要使用 -parameters 编译你的 Java 8+ 项目,以使命名参数在不使用额外注解的情况下正常工作。Spring Boot 的 Maven 和 Gradle 插件会自动为你完成此操作。如果由于某些原因无法实现,你可以添加 @Param 并显式指定名称,或者使用参数索引。

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

给定了一个 MovieVertexActor 类,其注解如电影模型中所示:

@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);
}
java

Movie 的实例传递给上述的存储库方法,将会生成以下 Neo4j 映射参数:

{
"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
}
}
}
json

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

提示

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

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

自定义查询中的值表达式

自定义查询中的 Spring 表达式语言

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

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

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);
}
java

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

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

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);
}
java
  • 在 SpEL 上下文中,Pageable 的名称始终为 pageable

  • 在 SpEL 上下文中,Sort 的名称始终为 sort

Spring 表达式语言扩展

字面量扩展

literal 扩展可以用于在自定义查询中使标签或关系类型等变得“动态”。在 Cypher 中,标签和关系类型都不能参数化,因此它们必须以字面量的形式给出。

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

@Query("MATCH (n:`:#{literal(#label)}`) RETURN n") 1
List<Inheritance.BaseClass> findByLabel(String label);
}
java
  • 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);
}
java

引用标签

你已经知道如何将 Node 映射到一个领域对象:

@Node(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
public class BikeNode {
@Id String id;

String name;
}
java

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

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);
}
java

该查询将解析为

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

注意我们如何为 nameOrId 使用了标准参数:在大多数情况下,没有必要通过添加 SpEL 表达式来使这里复杂化。

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

Spring 的属性占位符可以在自定义查询中使用,格式为 ${}

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

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