最新の安定バージョンについては、Spring Data Neo4j 7.4.4 を使用してください!

カスタムクエリ

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"})       |
+------------------------------------------------------------------------------------------+

マッピングの結果は使用できない可能性が高くなります。これがリストにマッピングされる場合、Movie の重複が含まれることになりますが、このムービーには 1 つの関連しかありません。

ルートノードごとに 1 つのレコードを取得する

正しいオブジェクトを取得するには、クエリ内のリレーションシップと関連ノードを収集する必要があります: 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),...] |
+------------------------------------------------------------------------+

この結果を 1 つのレコードとして使用すると、Spring Data Neo4j はすべての関連ノードをルートノードに正しく追加できます。

グラフをより深く理解する

上の例では、関連ノードの最初のレベルのみをフェッチしようとしていると想定しています。これでは不十分な場合があり、マップされたインスタンスの一部である必要があるノードがグラフのさらに深いところに存在する可能性があります。これを実現するには、データベース側またはクライアント側の削減の 2 つの方法があります。

このため、上記の例には、最初の Movie とともに返される Persons 上の Movies も含まれている必要があります。

image$movie graph deep
図 1: 「マトリックス」と「キアヌリーブス」の例

データベース側の削減

Spring Data Neo4j はレコードベースのみを適切に処理できることに留意し、1 つのエンティティインスタンスの結果は 1 つのレコードに含まれる必要があります。サイファーの道 (英語) 機能の使用は、グラフ内のすべての ブランチをフェッチする有効なオプションです。

単純なパスベースのアプローチ
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN p;

これにより、複数のパスが 1 つのレコード内でマージされなくなります。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);

「マトリックス」から別のムービーに至る経路は複数あるため、結果は依然として 1 つの記録にはなりません。ここで 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 関数を使用すると、さまざまなパスからのノードと関連を平坦化できます。結果として、ルートノードごとに 1 つのレコードを取得するに似たタプルが得られますが、コレクション内に関連型またはノードが混在しています。

クライアント側の削減

クライアント側で削減が行われる必要がある場合、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 を含む) は常に、マップされるルートノードとの一致を試みます。それ以降、すべての関連オブジェクトがマッピングされます。1 つのオブジェクトのみを返す必要があるクエリでは、そのルートオブジェクトが返されます。多くのオブジェクトを返すクエリでは、一致するすべてのオブジェクトが返されます。返されたオブジェクトからの出力および受信関連には、当然のことながら値が設定されます。

次の Cypher クエリを想定します。

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

これはルートノードごとに 1 つのレコードを取得するの推奨に従っており、ここで一致させたいリーフノードに最適です。ただし、これは、0 または 1 個のマップされたオブジェクトを返すすべてのシナリオにのみ当てはまります。このクエリは以前と同様にすべてのリレーションシップを設定しますが、4 つのオブジェクトすべてを返すわけではありません。

これは、パス全体を返すことで変更できます。

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

ここでは、パス p が実際に 4 つのノードすべてへのパスを含む 3 行を返すという事実を利用します。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 などを使用することもできます。
名前付きパラメーターを追加のアノテーションなしで機能させるには、Java 8+ プロジェクトを -parameters でコンパイルする必要があります。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 型を持ち、それ以上の変換は必要ありません。

すべての関連はマップのリストです。動的関連はそれに応じて解決されます。1 対 1 の関連もシングルトンリストとして直列化されます。人々間の 1 対 1 のマッピングにアクセスするには、これを $person.__properties__.BEST_FRIEND[0].__target__.__id__ と書くことになります。

エンティティが、異なる型の他のノードと同じ型の関連を持っている場合、すべて同じリストに表示されます。このようなマッピングが必要で、カスタムパラメーターを操作する必要がある場合は、それに応じてマッピングを展開する必要があります。これを行う 1 つの方法は、相関サブクエリです (Neo4j 4.1+ が必要です)。

カスタムクエリの Spring 式言語

Spring 式言語 (SpEL) は、:#{} 内のカスタムクエリで使用できます。ここでのコロンはパラメーターを指しており、パラメーターが意味をなす場合にはそのような式を使用する必要があります。ただし、リテラル拡張機能を使用すると、標準の Cypher でパラメーターが許可されない場所 (ラベルや関連型など) で SpEL 式を使用できます。これは、SpEL 評価を受けるクエリ内のテキストブロックを定義する標準的な Spring Data 方法です。

次の例では、基本的に上記と同じクエリを定義しますが、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 は :#{ で始まり、指定された String パラメーターを名前 (#pt1) で参照します。これを上記の Cypher 構文と混同しないでください。SpEL 式は、両方のパラメーターを 1 つの値に連結し、最終的に付録 /neo4j-client.adoc#neo4j-client に渡します。SpEL ブロックは } で終わります。

SpEL は、さらに 2 つの問題も解決します。Sort オブジェクトをカスタムクエリに渡すことができる 2 つの拡張機能が提供されています。カスタムクエリfaq.adoc# カスタムクエリとページとスライスの例を覚えていますか ? orderBy 拡張機能を使用すると、動的ソートを備えた Pageable をカスタムクエリに渡すことができます。

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);
}
1Pageable は、SpEL コンテキスト内では常に pageable という名前を持ちます。
2Sort は、SpEL コンテキスト内では常に sort という名前を持ちます。

Spring 式言語拡張機能

リテラル拡張

literal 拡張機能を使用すると、カスタムクエリでラベルや関連型などを「動的」にすることができます。Cypher ではラベルも関連型もパラメーター化できないため、リテラルを指定する必要があります。

リテラル拡張子
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {

    @Query("MATCH (n:`:#{literal(#label)}`) RETURN n") (1)
    List<Inheritance.BaseClass> findByLabel(String label);
}
1literal 拡張子は、評価されたパラメーターのリテラル値に置き換えられます。

ここでは、ラベル上で動的に一致させるために 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(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
public class BikeNode {
    @Id String id;

    String name;
}

このノードにはいくつかのラベルがあり、カスタムクエリで常に繰り返すとエラーが発生しやすくなります。1 つを忘れたり、型ミスをしたりする可能性があります。これを軽減するために、#{#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 式を追加して物事を複雑にする必要はありません。