如需最新的稳定版本,请使用 Spring Data Neo4j 7.4.0! |
自定义查询
与所有其他 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;
这将导致多个路径未合并到一条记录中。
可以调用,但 Spring Data Neo4j 不理解 Map 过程中的 paths 概念。
因此,需要提取节点和关系才能获得结果。collect(p)
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;
该函数允许我们从各种路径展平节点和关系。
因此,我们将获得一个元组,类似于 Getting one record per root node (为每个根节点获取一条记录),但在集合中混合了关系类型或节点。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));
additional 语句按以下格式创建列表:collect
[[rel1, rel2], [rel3, rel4]]
现在,这些列表将在映射过程中转换为简单列表。
决定是使用客户端还是数据库端缩减取决于将生成的数据量。
使用该函数时,需要首先在数据库的内存中创建所有路径。
另一方面,需要在 Client 端合并的大量数据会导致更高的内存使用率。reduce |
使用路径填充和返回实体列表
给定的图表如下所示:
以及映射中所示的域模型(为简洁起见,省略了构造函数和访问器):
@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
在这里,我们确实想利用 path 实际上返回 3 行以及所有 4 个节点的路径这一事实。所有 4 个节点都将是
填充、链接和返回。p
自定义查询中的参数
你这样做的方式与在 Neo4j 浏览器或 Cypher-Shell 中发出的标准 Cypher 查询完全相同。
使用语法(从 Neo4j 4.0 开始,Cypher 参数的旧语法已从数据库中删除)。$
${foo}
public interface ARepository extends Neo4jRepository<AnAggregateRoot, String> {
@Query("MATCH (a:AnAggregateRoot {name: $name}) RETURN a") (1)
Optional<AnAggregateRoot> findByCustomQuery(String name);
}
1 | 这里我们按参数名称引用参数。
您也可以改用 etc.$0 |
您需要编译 Java 8+ 项目,以使命名参数无需进一步的注释即可工作。
Spring Boot Maven 和 Gradle 插件会自动为您执行此操作。
如果由于任何原因不可行,则可以显式添加并指定名称或使用 parameters 索引。-parameters @Param |
映射的实体(所有带有 )作为参数传递给带有 Comments 的函数
自定义查询将转换为嵌套映射。
以下示例将结构表示为 Neo4j 参数。@Node
给定的是 a ,以及注释的类,如 movie 模型中所示:Movie
Vertex
Actor
@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);
}
将 的实例传递给上面的 repository 方法,将生成以下 Neo4j map 参数:Movie
{
"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 将始终包含 which 是 mapped id 属性。
在所有标签下,static 和 dynamic 将可用。
所有属性和关系类型都显示在这些映射中,就像当实体
由 SDN 编写。
值将具有正确的 Cypher 类型,并且不需要进一步转换。id
labels
所有关系都是映射列表。动态关系将相应地得到解决。
一对一关系也将序列化为单一实例列表。因此,要访问一对一映射
在人与人之间,你会写这个 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 以名称 () 开头,然后通过名称 () 引用给定的参数。
不要将此与上述 Cypher 语法混淆!
SPEL 表达式将两个参数连接成一个值,该值最终传递给 appendix/neo4j-client.adoc#neo4j-client。
SpEL 块以 .:#{
String
#pt1
}
SPEL 还解决了另外两个问题。我们提供了两个扩展,允许将对象传递到自定义查询中。
还记得自定义查询中的 faq.adoc#custom-queries-with-page-and-slice-examples 吗?
使用扩展,您可以向自定义查询传入 with dynamic sort:Sort
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);
}
1 | A 在 SPEL 上下文中始终具有名称。Pageable pageable |
2 | A 在 SPEL 上下文中始终具有名称。Sort sort |
Spring 表达式语言扩展
文本扩展
该扩展可用于在自定义查询中使标签或关系类型等内容“动态”。
标签和关系类型都不能在 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 | 扩展名将替换为 evaluated 参数的 Literal 值。literal |
此处,该值用于在 Label 上动态匹配。
如果作为参数传入方法,将生成 。已添加刻度以正确转义值。SDN 不会执行此操作
对您来说,这可能不是您在所有情况下都想要的。literal
SomeLabel
MATCH (n:
SomeLabel
) RETURN n
列出扩展
对于多个值,there are and in place 将呈现
所有值的 A 或 concatenated list 的别名。allOf
anyOf
&
|
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(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
public class BikeNode {
@Id String id;
String name;
}
这个节点有几个标签,在自定义查询中一直重复它们很容易出错:你可以
忘记一个或打错字。我们提供以下表达式来缓解这种情况:.请注意,这个 API 不是以冒号开头的!您可以在注释有 :#{#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
请注意我们是如何将 standard 参数用于 :在大多数情况下,这里没有必要让事情复杂化
添加 SPEL 表达式。nameOrId