此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Data Neo4j 7.4.4! |
自定义查询
与所有其他 Spring Data 模块一样,Spring Data Neo4j 允许您在存储库中指定自定义查询。 如果您无法通过派生的查询函数表达 finder logic ,这些会派上用场。
因为 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"}) | +------------------------------------------------------------------------------------------+
映射的结果很可能不可用。
如果这将映射到一个列表中,它将包含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 可以将所有相关节点正确地添加到根节点。
更深入地了解图表
上面的示例假定您只尝试获取第一级相关节点。 这有时是不够的,图形中更深处的节点也应该成为映射实例的一部分。 有两种方法可以实现此目的:数据库端或客户端缩减。
为此,上面的示例还应包含Movies
在Persons
返回初始Movie
.

数据库端缩减
请记住, 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 不理解 Map 过程中的 paths 概念。
因此,需要提取节点和关系才能获得结果。
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN m, nodes(p), relationships(p);
因为有多条路径从“The Matrix”通向另一部电影,所以结果仍然不会是一条记录。 这就是 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
function 允许我们从各种路径展平节点和关系。
因此,我们将获得一个元组,类似于 Getting one record per root node (为每个根节点获取一条记录),但在集合中混合了关系类型或节点。
客户端缩减
如果减少应该发生在客户端,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
statement 按以下格式创建列表:
[[rel1, rel2], [rel3, rel4]]
现在,这些列表将在映射过程中转换为简单列表。
决定是使用客户端还是数据库端缩减取决于将生成的数据量。
所有路径都需要先在数据库的内存中创建,当reduce 函数。
另一方面,需要在 Client 端合并的大量数据会导致更高的内存使用率。 |
使用路径填充和返回实体列表
给定的图表如下所示:

以及映射中所示的域模型(为简洁起见,省略了构造函数和访问器):
@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;
}
正如你所看到的,关系只是外向的。生成的 finder 方法(包括findById
) 将始终尝试匹配
要映射的根节点。从那里开始,所有相关对象都将被映射。在只应返回一个对象的查询中,
返回该根对象。在返回许多对象的查询中,将返回所有匹配的对象。传出和传入关系
当然,从返回的这些对象中填充。
假设以下 Cypher 查询:
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
RETURN leaf, collect(nodes(p)), collect(relationships(p))
它遵循 为每个根节点获取一条记录 中的建议,并且非常适合叶节点 您希望在此处匹配。但是:这仅适用于返回 0 或 1 个映射对象的所有方案。 虽然该查询将像以前一样填充所有关系,但它不会返回所有 4 个对象。
这可以通过返回整个 path 来更改:
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
RETURN p
在这里,我们确实希望使用路径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);
}
1 | 这里我们按参数名称引用参数。
您还可以使用$0 等等。 |
您需要使用-parameters 使命名参数无需进一步的注释即可工作。
Spring Boot Maven 和 Gradle 插件会自动为您执行此作。
如果由于任何原因这不可行,您可以添加@Param 并显式指定名称或使用 parameters 索引。 |
映射实体(所有带有@Node
) 作为参数传递给带有
自定义查询将转换为嵌套映射。
以下示例将结构表示为 Neo4j 参数。
Given 是Movie
,Vertex
和Actor
类的注释方式,如 Movie 模型中所示:
@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
到上面的 repository 方法中,将生成以下 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 表示。映射将始终包含id
这是 Mapped ID 属性。
下labels
所有标签(静态和动态)都将可用。
所有属性和关系类型都显示在这些映射中,就像当实体
由 SDN 编写。
值将具有正确的 Cypher 类型,并且不需要进一步转换。
所有关系都是映射列表。动态关系将相应地得到解决。
一对一关系也将序列化为单一实例列表。因此,要访问一对一映射
在人与人之间,你会写这个 DAS$person.__properties__.BEST_FRIEND[0].__target__.__id__ . |
如果一个实体与不同类型的其他节点具有相同的类型关系,则它们都将出现在同一个列表中。 如果您需要这样的映射,并且还需要使用这些自定义参数,则必须相应地展开它。 一种方法是关联子查询(需要 Neo4j 4.1+)。
自定义查询中的值表达式
自定义查询中的 Spring 表达式语言
Spring 表达式语言 (SpEL) 可用于内部的自定义查询:#{}
.
此处的冒号是指参数,应在参数有意义的地方使用此类表达式。
但是,当使用我们的文字扩展时,您可以在标准 Cypher
不允许使用参数(例如标签或关系类型)。
这是 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);
}
阻止的 SpEL 以:#{
然后引用给定的String
按名称 (#pt1
).
不要将此与上述 Cypher 语法混淆!
SPEL 表达式将两个参数连接成一个值,该值最终传递给 appendix/neo4j-client.adoc#neo4j-client。
SpEL 块以 .}
SPEL 还解决了另外两个问题。我们提供了两个扩展,允许传入一个Sort
object 转换为自定义查询。
还记得自定义查询中的 faq.adoc#custom-queries-with-page-and-slice-examples 吗?
使用orderBy
extension 中,您可以在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);
}
1
A Pageable
has always the name pageable
inside the SpEL context.
2
A Sort
has always the name sort
inside the SpEL context.
Spring Expression Language extensions
Literal extension
The literal
extension can be used to make things like labels or relationship-types "dynamic" in custom queries.
Neither labels nor relationship types can be parameterized in Cypher, so they must be given literal.
literal-Extension
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
@Query("MATCH (n:`:#{literal(#label)}`) RETURN n") (1)
List<Inheritance.BaseClass> findByLabel(String label);
}
1
The literal
extension will be replaced with the literal value of the evaluated parameter.
Here, the literal
value has been used to match dynamically on a Label.
If you pass in SomeLabel
as a parameter to the method, MATCH (n:SomeLabel
) RETURN n
will be generated. Ticks have been added to correctly escape values. SDN won’t do this
for you as this is probably not what you want in all cases.
List extensions
For more than one value there are allOf
and anyOf
in place that would render
either a or &
|
concatenated list of all values.
List extensions
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);
}
Referring to Labels
You already know how to map a Node to a domain object:
A Node with many labels
@Node(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
public class BikeNode {
@Id String id;
String name;
}
This node has a couple of labels, and it would be rather error prone to repeat them all the time in custom queries: You might
forget one or make a typo. We offer the following expression to mitigate this: #{#staticLabels}
. Notice that this one does
not start with a colon! You use it on repository methods annotated with @Query
:
#{#staticLabels}
in action
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);
}
This query will resolve to
MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.id = $nameOrId OR n.name = $nameOrId RETURN n
Notice how we used standard parameter for the nameOrId
: In most cases there is no need to complicate things here by
adding a SpEL expression.
Property Placeholder resolution in custom queries
Spring’s property placeholders can be used in custom queries inside .${}
ARepository.java
@Query("MATCH (a:AnAggregateRoot) WHERE a.name = :${foo} RETURN a")
Optional<AnAggregateRoot> findByCustomQueryWithPropertyPlaceholder();
In the example above, if the property foo
would be set to bar
then the ${foo}
block would be resolved to bar
.