自定義查詢
Spring Data Neo4j,與其他 Spring Data 模組一樣,允許您在倉庫中指定自定義查詢。當您無法透過派生查詢函式表達查詢邏輯時,自定義查詢會非常有用。
因為 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"}) | +------------------------------------------------------------------------------------------+
對映的結果很可能無法使用。如果這被對映到一個列表中,則會包含電影的重複項,但這部電影只會有一個關係。
每個根節點獲取一條記錄
要獲取正確的物件,需要在查詢中 collect
(收集)關係和相關節點: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 可以將所有相關節點正確地新增到根節點。
深入圖
上面的示例假設您只嘗試獲取第一級相關節點。有時這不足夠,圖中有更深層的節點也應作為對映例項的一部分。有兩種方法可以實現這一點:資料庫端規約或客戶端規約。
為此,上面的示例中,透過初始電影返回的人員也應包含他們參演的電影。

資料庫端規約
請記住,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 在對映過程中不理解路徑的概念。因此,需要從結果中提取節點和關係。
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 允許您對映列表的列表,這些列表可以是關係或節點。儘管如此,仍然適用要求,即返回的記錄應包含所有資訊,以正確地填充結果實體例項。
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 Browser 或 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 | 這裡我們透過引數名稱來引用它。您也可以使用 $0 等替代。 |
您需要使用 -parameters 引數編譯 Java 8+ 專案,以便命名引數無需額外註解即可工作。Spring Boot 的 Maven 和 Gradle 外掛會自動為您執行此操作。如果由於某種原因這不可行,您可以新增 @Param 並顯式指定名稱,或使用引數索引。 |
標記為 @Node
的對映實體(所有帶有 @Node
的內容)作為引數傳遞給帶有自定義查詢註解的函式時,將被轉換為巢狀的 map。以下示例展示了作為 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 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 表示。該 map 將始終包含 __id__
,這是對映的 id 屬性。在 __labels__
下,所有標籤(靜態和動態)都將可用。所有屬性以及關係型別,都將出現在這些 map 中,就像實體由 SDN 寫入圖資料庫時那樣。值將具有正確的 Cypher 型別,無需進一步轉換。
所有關係都是 map 的列表。動態關係將相應地被解析。一對一關係也將被序列化為單元素列表。因此,要訪問人員之間的一對一對映,您可以這樣編寫:$person.__properties__.BEST_FRIEND[0].__target__.__id__ 。 |
如果一個實體與不同型別的其他節點具有相同型別的關係,它們都將出現在同一個列表中。如果您需要這樣的對映,並且也需要使用這些自定義引數,則必須相應地展開它。一種方法是使用相關子查詢(需要 Neo4j 4.1+)。
自定義查詢中的值表示式
自定義查詢中的 Spring Expression Language
Spring Expression Language (SpEL) 可以在自定義查詢的 :#{}
內部使用。這裡的冒號表示一個引數,這樣的表示式應該在引數有意義的地方使用。然而,當使用我們的 literal 擴充套件時,您可以在標準 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 表示式將兩個引數連線成一個單一值,最終傳遞給 附錄/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 Expression Language 擴充套件
Literal 擴充套件
literal
擴充套件可用於在自定義查詢中使標籤或關係型別等內容“動態化”。標籤和關係型別都不能在 Cypher 中引數化,因此必須以 literal 形式提供。
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
@Query("MATCH (n:`:#{literal(#label)}`) RETURN n") (1)
List<Inheritance.BaseClass> findByLabel(String label);
}
1 | literal 擴充套件將被評估引數的 literal 值替換。 |
這裡,literal
值已用於動態匹配標籤。如果將 SomeLabel
作為引數傳遞給方法,將生成 MATCH (n:`SomeLabel`) RETURN n
。已新增反引號以正確轉義值。SDN 不會為您執行此操作,因為這可能並非所有情況下都符合您的需求。
列表擴充套件
對於多個值,可以使用 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;
}
這個節點有幾個標籤,在自定義查詢中一直重複它們容易出錯:您可能會忘記或拼寫錯誤。我們提供以下表達式來緩解此問題:#{#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 表示式來使事情複雜化。