|
该版本仍在开发中,尚未被视为稳定。对于最新稳定版本,请使用Spring Data Neo4j 8.0.0! |
自定义查询
Spring Data Neo4j 和其他所有 Spring Data 模块一样,允许你在仓库中指定自定义查询。 如果你无法通过导出查询函数表达查找逻辑,这些功能非常有用。
由于 Spring Data Neo4j 内部高度面向记录,因此必须牢记这一点,不要为同一“根节点”构建多个记录的结果集。
| 请也查看常见问题解答,了解使用库中自定义查询的替代方式,特别是 如何使用自定义查询与自定义映射:自定义查询和自定义映射。 |
带有关系的查询
小心笛卡尔积
假设你有类似的查询MATCH (m:Movie{标题:“黑客帝国”})←[r:ACTED_IN]-(p:Person) 返回 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"}) |
+------------------------------------------------------------------------------------------+
映射的结果很可能无法使用。
如果这个列表被映射到列表中,它会包含电影但这部电影只会有一段关系。
每个根节点获取一条记录
要返回正确的对象,需要收集查询中的关系和相关节点:MATCH (m:电影{标题:“黑客帝国”})←[r:ACTED_IN]-(p:Person) 返回 m,collect(r),collect(p)
+------------------------------------------------------------------------+ | m | collect(r) | collect(p) | +------------------------------------------------------------------------+ | (:Movie) | [[:ACTED_IN], [:ACTED_IN], ...]| [(:Person), (:Person),...] | +------------------------------------------------------------------------+
有了这个结果作为单一记录,Spring Data Neo4j 可以正确地将所有相关节点添加到根节点。
深入分析图表
上面的例子假设你只试图获取相关节点的第一层。 这有时不够,图中可能还有更深的节点也应该包含在映射实例中。 实现这一点有两种方式:数据库端或客户端减少。
为此,上述示例还应包含电影在人会被带回首字母电影.
数据库端简化
请记住,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的简化函数发挥作用的地方。
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 Data Neo4j 还能映射关系列表或节点列表。 不过,返回记录必须包含所有信息,以正确水合结果实体实例。
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN m, collect(nodes(p)), collect(relationships(p));
附加收集语句创建列表格式如下:
[[rel1, rel2], [rel3, rel4]]
这些列表将在映射过程中被转换为平面列表。
决定是选择客户端还是数据库端减少,取决于生成的数据量。
当减少函数被使用。
另一方面,客户端需要合并大量数据会导致内存使用增加。 |
利用路径填充并返回实体列表
给出一张图,看起来如下:
以及如映射所示的领域模型(为简洁起见省略了构造子和访问器):
@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)总是会尽量匹配
一个需要映射的根节点。从那时起,所有相关对象都会被映射。在只应返回一个对象的查询中,
该根对象被返回。在返回多个对象的查询中,所有匹配的对象都会返回。离婚与新入的关系
从这些返回的物体中,当然会被填满。
假设以下密码查询:
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 浏览器或 Cypher-Shell 中发出的标准密码查询完全相同,
语法(从 Neo4j 4.0 及以后,旧版为$${foo}密码参数的语法已被从数据库中移除)。
public interface ARepository extends Neo4jRepository<AnAggregateRoot, String> {
@Query("MATCH (a:AnAggregateRoot {name: $name}) RETURN a") (1)
Optional<AnAggregateRoot> findByCustomQuery(String name);
}
| 1 | 这里我们以参数名称来称呼。
你也可以使用0美元等等。 |
你需要编译你的 Java 8+ 项目-参数使命名参数无需额外注释即可工作。
Spring Boot Maven 和 Gradle 插件会自动帮你完成这个作。
如果这不可行,你可以选择添加@Param并明确指定名称或使用参数索引。 |
映射实体(所有带有@Node)作为参数传递给一个注释为
自定义查询将被转化为嵌套映射。
以下示例将结构表示为Neo4j参数。
给定为电影,顶点和演员类标注如下,如电影模型所示:
@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);
}
传递 的实例电影对上述存储库方法,将生成以下 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属性。
下__标签__所有标签,无论是静态还是动态,都将开放。
所有属性和关系类型都会在这些映射中以实体在图中出现的方式出现
由SDN编写。
这些值会有正确的密码类型,无需进一步转换。
所有关系都是地图列表。动态关系将相应地解决。
一对一关系也会以单例列表的形式序列化。因此,要实现一对一的映射
人与人之间,你会写下这句 DAS$person.__properties__。BEST_FRIEND[0].__target__.__id__. |
如果一个实体与同一类型的其他节点有不同类型的关系,它们都会出现在同一个列表中。 如果你需要这样的映射,同时又需要处理这些自定义参数,就必须相应地展开它。 一种方法是相关的子查询(需要 Neo4j 4.1+)。
自定义查询中的值表达式
自定义查询中的 Spring 表达式语言
Spring表达式语言(SpEL)可以在内部自定义查询中使用:#{}.
这里的冒号指的是一个参数,应在参数合理时使用这样的表达式。
然而,使用我们的字面扩展时,你可以在标准密码的情况下使用 SpEL 表达式
不允许参数(比如标签或关系类型)。
这是Spring Data定义在接受SpEL评估的查询中文本块的标准方法。
以下示例基本上定义了与上述相同的查询,但使用哪里避免更多卷括的条款:
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).
不要把它和上面提到的密码语法混淆!
SpEL 表达式将两个参数串接为一个单一值,最终传递给 appendix/neo4j-client.adoc#neo4j-client。
SpEL块以 结尾。}
SpEL还解决了另外两个问题。我们提供两个扩展,允许通过排序对象被自定义查询。
还记得自定义查询中的 faq.adoc#custom-queries-with-page-and-slice-examples 吗?
与顺序By你可以在可页面通过动态排序到自定义查询:
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语境内。 |
| 2 | 一个排序一直以来排序在SpEL语境内。 |
Spring 表达式语言扩展
字面扩展
这字面扩展可以用来让标签或关系类型在自定义查询中变得“动态”。
Cypher 中标签和关系类型都不能参数化,因此必须以字面形式表示。
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
@Query("MATCH (n:`:#{literal(#label)}`) RETURN n") (1)
List<Inheritance.BaseClass> findByLabel(String label);
}
| 1 | 这字面扩展将被替换为计算参数的字面值。 |
这里,字面value 被用于在标签上动态匹配。
如果你经过SomeLabel作为该方法的一个参数,匹配 (n:将生成。为了正确转义值,已添加刻度。SDN不会这样做
对你来说,这可能不是你所有情况下想要的。SomeLabel) 返回 n
列表扩展
对于多个值,有全部和任何人在那个地方会渲染
要么是 a 或&|所有值的串接列表。
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
注意我们如何使用标准参数名字OrId:在大多数情况下,无需通过以下方式复杂化
添加一个 SpEL 表达式。