自訂查詢

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 可以正確地將所有相關節點新增至根節點。

深入圖形

上面的範例假設您只嘗試擷取第一層的相關節點。這有時不足夠,圖形中可能還有更深層的節點也應該是映射實例的一部分。有兩種方法可以實現此目的:資料庫端縮減或用戶端縮減。

為此,上述範例也應包含 PersonsMovies,這些電影會與初始 Movie 一起傳回。

image$movie graph deep
圖 1. 「駭客任務」和「基努·李維」範例

資料庫端縮減

請記住,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 讓您也可以映射關聯或節點的列表列表。儘管如此,要求仍然適用於傳回的記錄應包含所有資訊,以正確地水合產生的實體實例。

從路徑收集節點和關聯
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 函數時,所有路徑都需要先在資料庫的記憶體中建立。另一方面,需要在用戶端合併的大量資料會導致那裡的記憶體使用量更高。

使用路徑來填入並傳回實體列表

給定一個看起來像這樣的圖形

image$custom query.paths
圖 2. 具有外向關聯的圖形

以及 映射 中顯示的網域模型 (為簡潔起見,省略了建構子和存取子)

具有外向關聯的圖形的網域模型。
@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 瀏覽器或 Cypher-Shell 中發出的標準 Cypher 查詢完全相同,使用 $ 語法 (從 Neo4j 4.0 開始,用於 Cypher 參數的舊 ${foo} 語法已從資料庫中移除)。

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 Boot Maven 和 Gradle 外掛程式會自動為您執行此操作。如果由於任何原因而不可行,您可以新增 @Param 並明確指定名稱,或使用參數索引。

作為參數傳遞給使用自訂查詢註解的函數的映射實體 (所有帶有 @Node 的實體) 將轉換為巢狀映射。以下範例表示為 Neo4j 參數的結構。

給定在 電影模型 中顯示的註解的 MovieVertexActor 類別

「標準」電影模型
@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 運算式語言

Spring 運算式語言 (SpEL) 可在自訂查詢中的 :#{} 內使用。此處的冒號指的是參數,並且應在參數有意義的地方使用此類運算式。但是,當使用我們的 常值擴充功能 時,您可以在標準 Cypher 不允許參數的位置 (例如用於標籤或關聯類型) 使用 SpEL 運算式。這是 Spring Data 定義查詢內經歷 SpEL 評估的文字區塊的標準方式。

以下範例基本上定義了與上述相同的查詢,但使用 WHERE 子句以避免更多大括號

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 區塊以 :#{ 開頭,然後依名稱 (#pt1) 參考給定的 String 參數。不要將其與上述 Cypher 語法混淆!SpEL 運算式將兩個參數串連成單一值,最終傳遞至 appendix/neo4j-client.adoc#neo4j-client。SpEL 區塊以 } 結尾。

SpEL 也解決了另外兩個問題。我們提供兩個擴充功能,允許將 Sort 物件傳遞至自訂查詢。還記得 faq.adoc#custom-queries-with-page-and-slice-examples 中的 自訂查詢 嗎?使用 orderBy 擴充功能,您可以將具有動態排序的 Pageable 傳遞至自訂查詢

orderBy-擴充功能
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 中參數化,因此它們必須以常值形式給定。

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 值已用於動態比對標籤。如果您將 SomeLabel 作為參數傳遞給方法,則會產生 MATCH (n:SomeLabel) RETURN n。已新增刻度線以正確逸出值。SDN 不會為您執行此操作,因為這可能並非您在所有情況下都想要的操作。

列表擴充功能

對於多個值,有 allOfanyOf 可以呈現 &| 串連的所有值列表。

列表擴充功能
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 運算式來使事情複雜化。

自訂查詢中的屬性預留位置解析

Spring 的屬性預留位置可以在自訂查詢中的 ${} 內使用。

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

在上面的範例中,如果屬性 foo 設定為 bar,則 ${foo} 區塊將解析為 bar