自定義查詢

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

深入圖

上面的示例假設您只嘗試獲取第一級相關節點。有時這不足夠,圖中有更深層的節點也應作為對映例項的一部分。有兩種方法可以實現這一點:資料庫端規約或客戶端規約。

為此,上面的示例中,透過初始電影返回的人員也應包含他們參演的電影。

image$movie graph deep
圖 1. 《駭客帝國》和基努·裡維斯的示例

資料庫端規約

請記住,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 函式時,所有路徑都需要首先在資料庫記憶體中建立。另一方面,大量需要在客戶端合併的資料會導致客戶端更高的記憶體使用。

使用路徑填充並返回實體列表

假設圖結構如下:

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 Browser 或 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 的對映實體(所有帶有 @Node 的內容)作為引數傳遞給帶有自定義查詢註解的函式時,將被轉換為巢狀的 map。以下示例展示了作為 Neo4j 引數的結構。

假設 Movie, VertexActor 類如 電影模型 中所示進行註解。

“標準”電影模型
@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 子句以避免更多花括號:

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 表示式將兩個引數連線成一個單一值,最終傳遞給 附錄/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 Expression Language 擴充套件

Literal 擴充套件

literal 擴充套件可用於在自定義查詢中使標籤或關係型別等內容“動態化”。標籤和關係型別都不能在 Cypher 中引數化,因此必須以 literal 形式提供。

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 不會為您執行此操作,因為這可能並非所有情況下都符合您的需求。

列表擴充套件

對於多個值,可以使用 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 對映到領域物件

帶有多個標籤的 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 表示式來使事情複雜化。

自定義查詢中的屬性佔位符解析

Spring 的屬性佔位符可以在自定義查詢的 ${} 內部使用。

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

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