自定义查询
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。
数据库端减少
请记住,Spring Data Neo4j 只能正确地处理基于记录的数据,一个实体实例的结果需要存在于一条记录中。使用 Cypher 的路径 功能是获取图谱中所有分支的有效选项。
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 使您能够映射关系或节点的列表的列表。然而,返回的记录应包含所有信息以正确地水合(hydrate)结果实体实例的要求仍然适用。
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 函数时,所有路径都需要首先在数据库内存中创建。另一方面,大量需要在客户端合并的数据会导致客户端内存使用量增加。 |
使用路径填充并返回实体列表
给定一个图,它看起来像这样
以及如 映射 中所示的领域模型(为简洁起见,已省略构造函数和访问器)
@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 4.0 开始,Cypher 参数的旧 ${foo} 语法已从数据库中删除),以与在 Neo4j Browser 或 Cypher-Shell 中发出的标准 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 编译您的 Java 8+ 项目,以便命名参数无需进一步注释即可工作。Spring Boot Maven 和 Gradle 插件会自动为您执行此操作。如果由于任何原因不可行,您可以添加 @Param 并明确指定名称,或使用参数索引。 |
作为参数传递给带有自定义查询注解的函数的映射实体(所有带有 @Node 的实体)将被转换为嵌套映射。以下示例表示 Neo4j 参数的结构。
给定 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);
}
将 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
}
}
}
节点由映射表示。该映射将始终包含 __id__,即映射的 id 属性。在 __labels__ 下,所有标签(静态和动态)都将可用。所有属性和关系类型都将出现在这些映射中,就像实体由 SDN 写入时在图谱中出现一样。值将具有正确的 Cypher 类型,无需进一步转换。
所有关系都是映射列表。动态关系将相应地解析。一对一关系也将序列化为单例列表。因此,要访问人与人之间的一对一映射,您可以编写 $person.__properties__.BEST_FRIEND[0].__target__.__id__。 |
如果一个实体与不同类型的其他节点具有相同类型的关系,它们都将出现在同一个列表中。如果您需要这样的映射并且还需要处理这些自定义参数,则必须相应地展开它。一种方法是相关子查询(需要 Neo4j 4.1+)。
自定义查询中的值表达式
自定义查询中的 Spring Expression Language
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);
}
SpEL 块以 :#{ 开始,然后按名称 (#pt1) 引用给定的 String 参数。不要与上面的 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);
}
| 1 | Pageable 在 SpEL 上下文中始终具有名称 pageable。 |
| 2 | Sort 在 SpEL 上下文中始终具有名称 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);
}
| 1 | literal 扩展将被评估参数的字面值替换。 |
这里,literal 值已用于动态匹配标签。如果您将 SomeLabel 作为参数传递给该方法,则将生成 MATCH (n:。已添加反引号以正确转义值。SDN 不会为您执行此操作,因为在所有情况下这可能不是您想要的。SomeLabel) RETURN n
列表扩展
对于多个值,存在 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(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 表达式来使事情复杂化。