此版本仍在开发中,目前尚不被视为稳定版本。如需最新稳定版本,请使用 Spring Data Neo4j 8.0.4spring-doc.cadn.net.cn

自定义查询

Spring Data Neo4j,与其他所有 Spring Data 模块一样,允许您在存储库中指定自定义查询。当您无法通过派生查询函数表达查找逻辑时,这些自定义查询会非常有用。spring-doc.cadn.net.cn

由于 Spring Data Neo4j 在底层主要采用记录导向(record-oriented)的方式工作,因此需要注意这一点,避免为同一个“根节点”构建包含多条记录的结果集。spring-doc.cadn.net.cn

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

带有关系的查询

注意笛卡尔积

假设您有一个查询,例如 MATCH (m:Movie{title: 'The Matrix'})←[r:ACTED_IN]-(p:Person) return m,r,p,其结果可能如下所示:spring-doc.cadn.net.cn

多条记录(已截断)
+------------------------------------------------------------------------------------------+
| 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处包含重复项,但这部电影只有一个关系。spring-doc.cadn.net.cn

获取每个根节点一条记录

To get the right object(s) back, it is required to 收集 the relationships and related nodes in the query: MATCH (m:Movie{title: 'The Matrix'})←[r:ACTED_IN]-(p:Person) return m,collect(r),collect(p)spring-doc.cadn.net.cn

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

有了这个结果作为单个记录,Spring Data Neo4j 可以正确地将所有相关的节点添加到根节点。spring-doc.cadn.net.cn

深入图的更深层次

<p>上面的例子假设您只想获取相关节点的第一级。</p> <p>这有时还不够,图中可能还有更深层次的节点也应该成为映射实例的一部分。</p> <p>有两种方法可以实现这一点:数据库端或客户端缩减。</p>spring-doc.cadn.net.cn

为了该练习,代码上面需要容压分量的路径的数组,待结果被ç»åº¦ä¸ä»¥JSONæ8 format!!spring-doc.cadn.net.cn

image$movie graph deep
图 1:“黑客帝国”和“基努·里维斯”的示例

基于数据库的归约

spring-doc.cadn.net.cn

考虑到Spring Data Neo4j只能正确处理基于记录的操作,对于一个实体实例的结果需要在一个记录中。spring-doc.cadn.net.cn

使用Cypher的路径功能是获取图中所有分支的有效选项。spring-doc.cadn.net.cn

spring-doc.cadn.net.cn

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

spring-doc.cadn.net.cn

这将导致多个路径,这些路径在一条记录中不会被合并。spring-doc.cadn.net.cn

可以调用collect(p),但是Spring Data Neo4j在映射过程中不理解路径的概念。spring-doc.cadn.net.cn

因此,在结果中需要提取节点和关系。spring-doc.cadn.net.cn

spring-doc.cadn.net.cn

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

因为从《黑客帝国》到另一部电影有多条路径,结果仍然不会是单个记录。 这就是 西弗的 reduce 函数 发挥作用的地方。spring-doc.cadn.net.cn

减少节点和关系
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;

spring-doc.cadn.net.cn

0 函数允许我们将来自各个路径的节点和关系展平。 spring-doc.cadn.net.cn

结果我们将得到一个元组,类似于 获取根节点每条记录 ,但集合中的关系类型或节点混合在一起。 spring-doc.cadn.net.cn

spring-doc.cadn.net.cn

减少客户端

如果减法应该在客户端发生,Spring Data Neo4j 允许您映射关系或节点的列表的列表。 同样,要求是返回的记录应包含正确反序列化生成实体实例的所有信息。spring-doc.cadn.net.cn

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

额外的 collect 语句会创建以下格式的列表:
spring-doc.cadn.net.cn

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

现在,在映射过程中,这些列表将被转换为一个扁平列表。spring-doc.cadn.net.cn

决定是选择客户端减少还是数据库端减少,取决于将生成的数据量。

spring-doc.cadn.net.cn

当使用reduce函数时,所有路径都必须首先在数据库的内存中创建。spring-doc.cadn.net.cn

另一方面,在客户端需要合并大量数据会导致那里的内存使用更高。spring-doc.cadn.net.cn

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

给定的图如下所示:spring-doc.cadn.net.cn

image$custom query.paths
图2. 具有传出关系的图形

并显示如下的< a href="0">映射(为简洁起见,省略了构造函数和访问器):spring-doc.cadn.net.cn

图的关联模型带有传出关系
@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)将始终尝试匹配一个根节点进行映射。从那里开始,所有相关对象都会被映射。在仅应返回一个对象的查询中,返回该根对象。在返回多个对象的查询中,会返回所有匹配的对象。当然,这些返回对象的传出和传入关系也会被填充。spring-doc.cadn.net.cn

假设以下Cypher查询:spring-doc.cadn.net.cn

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

它遵循了获取每个根节点一条记录的建议,对于您想要匹配的叶节点来说效果很好。然而:这只适用于返回0或1个映射对象的所有情况。虽然该查询会像之前一样填充所有关系,但它不会返回全部4个对象。spring-doc.cadn.net.cn

可以通过返回整个路径来更改此内容:spring-doc.cadn.net.cn

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

在这里,我们确实希望利用路径 p 实际上返回了3行数据的事实,这些数据是通往所有4个节点的路径。所有的4个节点都将被填充、相互连接并返回。spring-doc.cadn.net.cn

自定义查询中的参数

您执行此操作的方式与在标准 Cypher 查询中使用 Neo4j 浏览器或 Cypher-Shell 的方式完全相同,使用$语法(从 Neo4j 4.0 开始向上,数据库已删除旧的${foo}语法用于 Cypher 参数)。spring-doc.cadn.net.cn

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-doc.cadn.net.cn

Spring Boot 的 Maven 和 Gradle 插件会自动为您完成此操作。spring-doc.cadn.net.cn

如果由于任何原因无法实现这一点,您可以选择添加 @Param 并显式指定名称,或使用参数索引。spring-doc.cadn.net.cn

作为带有自定义查询注解的函数参数传递的映射实体(所有带@Node的内容)将转换为嵌套地图。以下示例表示Neo4j参数的结构。spring-doc.cadn.net.cn

给定的是用以下方式注解的MovieVertexActor电影模型spring-doc.cadn.net.cn

标准电影模型
@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 映射参数:spring-doc.cadn.net.cn

{
  "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类型出现,并且不需要进一步转换。spring-doc.cadn.net.cn

所有关系都是映射列表。动态关系将相应地被解析。

spring-doc.cadn.net.cn

一对一的关系也将序列化为单元素列表。因此,要访问人员之间的一对一映射,您将编写如下代码:$person.__properties__.BEST_FRIEND[0].__target__.__id__spring-doc.cadn.net.cn

如果一个实体与同一类型的其他节点具有关系,它们将全部出现在同一个列表中。<br/>如果您需要这种映射并且还需要处理这些自定义参数,则必须相应地展开它。<br/>一种实现方法是使用相关子查询(需要 Neo4j 4.1 或更高版本)。spring-doc.cadn.net.cn

自定义查询中的值表达式

在自定义查询中使用 Spring 表达式语言

Spring 表达式语言(SpEL) 可用于自定义查询中的 :#{}。这里的冒号指的是一个参数,这种表达式应该用在有意义的地方。
但是,当使用我们的文字扩展时,您可以在标准 Cypher 不允许使用参数的位置(例如标签或关系类型)中使用 SpEL 表达式。
这是 Spring Data 标准的方式,在查询中定义经过 SpEL 计算的文本块。spring-doc.cadn.net.cn

下面的示例基本上定义了与上面相同的查询,但使用WHERE子句来避免更多的花括号:spring-doc.cadn.net.cn

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 块以 :#{ 开始,然后通过名称(String)引用给定的 #pt1 参数。请勿将其与上面的 Cypher 语法混淆!SpEL 表达式将这两个参数连接成一个单一值,并最终传递到附录/neo4j-client.adoc#neo4j-client} 结束 SpEL 块。spring-doc.cadn.net.cn

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

orderBy-Extension
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 在 SpEL 上下文中,Pageable 始终具有名称 pageable
2 在 SpEL 上下文中,Sort 始终具有名称 sort

Spring 表达式语言扩展

字面量扩展

在自定义查询中,literal 扩展可用于使标签或关系类型“动态”。 在 Cypher 中无法对标签或关系类型进行参数化,因此必须提供字面量。spring-doc.cadn.net.cn

literal-Extension
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:SomeLabel) RETURN n。已经添加了刻度以正确转义值。SDN 不会为您做这件事,因为这可能不是在所有情况下您想要的结果。spring-doc.cadn.net.cn

列表扩展

对于多个值,allOfanyOf 的位置会渲染出一个连接所有值的列表,要么是 & 要么是 |spring-doc.cadn.net.cn

列表扩展
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);
}

参考标签

您已经知道如何将节点映射到域对象:spring-doc.cadn.net.cn

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

    String name;
}

此节点有几个标签,如果在自定义查询中反复编写这些标签会很容易出错:你可能会忘记其中一个或拼错。我们提供以下表达式来缓解这个问题:#{#staticLabels}。请注意,这个表达式并不以冒号开头!您可以在用 @Query 注解的存储库方法中使用它:spring-doc.cadn.net.cn

#{#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);
}

此查询将解析为spring-doc.cadn.net.cn

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

请注意我们是如何为nameOrId使用标准参数的:在大多数情况下,这里没有必要通过添加SpEL表达式来使事情复杂化。spring-doc.cadn.net.cn

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

Spring 的属性占位符可以在 ${} 内部的自定义查询中使用。spring-doc.cadn.net.cn

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

在上面的示例中,如果属性foo被设置为bar,那么${foo}块将被解析为barspring-doc.cadn.net.cn