Neo4jClient

Spring Data Neo4j には Neo4j クライアントが付属しており、Neo4j の Java ドライバーの上に薄い層を提供します。

プレーン Java ドライバー [GitHub] (英語) は、命令型およびリアクティブバージョンに加えて非同期 API を提供する非常に多用途のツールですが、Spring アプリケーションレベルのトランザクションとは統合されていません。

SDN は、慣用的なクライアントの概念を通じてドライバーを可能な限り直接的に使用します。

クライアントには次の主なゴールがあります

  1. 命令型シナリオとリアクティブシナリオの両方に対応する Springs トランザクション管理への統合

  2. 必要に応じて JTA トランザクションに参加します

  3. 命令型シナリオとリアクティブ型シナリオの両方に一貫した API を提供する

  4. マッピングのオーバーヘッドを追加しない

SDN はこれらすべての機能に依存し、使用してエンティティマッピング機能を実現します。

命令型とリアクティブの Neo4 クライアントがスタック内のどこに配置されているかについては、SDN の構成要素を参照してください。

Neo4j クライアントには 2 つの種類があります。

  • org.springframework.data.neo4j.core.Neo4jClient

  • org.springframework.data.neo4j.core.ReactiveNeo4jClient

どちらのバージョンも同じ語彙と構文を使用する API を提供しますが、API 互換性はありません。どちらのバージョンも、クエリを指定し、パラメーターをバインドし、結果を抽出するための同じ流れるような API を備えています。

命令的ですか、それともリアクティブ的ですか ?

Neo4j クライアントとの対話は通常、次の呼び出しで終了します。

  • fetch().one()

  • fetch().first()

  • fetch().all()

  • run()

命令型バージョンはこの時点でデータベースと対話し、リクエストされた結果または要約を Optional<> または Collection でラップして取得します。

対照的に、リアクティブバージョンは、リクエストされた型のパブリッシャーを返します。データベースとの対話と結果の取得は、パブリッシャーがサブスクライブされるまで行われません。パブリッシャーは 1 回だけサブスクライブできます。

クライアントのインスタンスを取得する

SDN のほとんどの場合と同様、両方のクライアントは構成されたドライバーインスタンスに依存します。

必須の Neo4j クライアントのインスタンスの作成
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;

import org.springframework.data.neo4j.core.Neo4jClient;

public class Demo {

    public static void main(String...args) {

        Driver driver = GraphDatabase
            .driver("neo4j://localhost:7687", AuthTokens.basic("neo4j", "secret"));

        Neo4jClient client = Neo4jClient.create(driver);
    }
}

ドライバーは、4.0 データベースに対してのみリアクティブセッションを開くことができ、それより低いバージョンでは例外が発生して失敗します。

リアクティブ Neo4j クライアントのインスタンスの作成
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;

import org.springframework.data.neo4j.core.ReactiveNeo4jClient;

public class Demo {

    public static void main(String...args) {

        Driver driver = GraphDatabase
            .driver("neo4j://localhost:7687", AuthTokens.basic("neo4j", "secret"));

        ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver);
    }
}
トランザクションを有効にした場合に備えて、Neo4jTransactionManager または ReactiveNeo4jTransactionManager の提供に使用したものと同じドライバーインスタンスをクライアントに使用してください。ドライバーの別のインスタンスを使用する場合、クライアントはトランザクションを同期できません。

当社の Spring Boot スターターは、環境 (命令型またはリアクティブ) に適合する Neo4j クライアントのすぐに使用できる Bean を提供しており、通常は独自のインスタンスを構成する必要はありません。

使用方法

ターゲットデータベースの選択

Neo4j クライアントは、Neo4j 4.0 のマルチデータベース機能とともに使用できるように十分に準備されています。特に指定しない限り、クライアントはデフォルトのデータベースを使用します。クライアントの流れるような API により、実行するクエリの宣言後にターゲットデータベースを 1 回だけ指定できます。ターゲットデータベースの選択は、リアクティブクライアントを使用してそれを示します。

ターゲットデータベースの選択
Flux<Map<String, Object>> allActors = client
	.query("MATCH (p:Person) RETURN p")
	.in("neo4j") (1)
	.fetch()
	.all();
1 クエリを実行するターゲットデータベースを選択します。

クエリの指定

クライアントとの対話はクエリから始まります。クエリはプレーン String または Supplier<String> によって定義できます。サプライヤーはできるだけ遅く評価され、任意のクエリビルダーによって提供できます。

クエリの指定
Mono<Map<String, Object>> firstActor = client
	.query(() -> "MATCH (p:Person) RETURN p")
	.fetch()
	.first();

結果の取得

前のリストが示すように、クライアントとの対話は常に fetch の呼び出しと受信される結果の数で終了します。リアクティブ型と強制型のクライアントオファーの両方

one()

クエリからは 1 つの結果だけが期待されます

first()

結果を期待して最初のレコードを返します

all()

返されたすべてのレコードを取得します

命令型クライアントはそれぞれ Optional<T> と Collection<T> を返しますが、リアクティブクライアントは Mono<T> と Flux<T> を返し、後者はサブスクライブされている場合にのみ実行されます。

クエリからの結果が期待できない場合は、クエリを指定した後に run() を使用します。

リアクティブな方法で結果の概要を取得する
Mono<ResultSummary> summary = reactiveClient
    .query("MATCH (m:Movie) where m.title = 'Aeon Flux' DETACH DELETE m")
    .run();

summary
    .map(ResultSummary::counters)
    .subscribe(counters ->
        System.out.println(counters.nodesDeleted() + " nodes have been deleted")
    ); (1)
1 実際のクエリは、パブリッシャーをサブスクライブすることによってここでトリガーされます。

少し時間を取って両方のリストを比較し、実際のクエリがトリガーされたときの違いを理解しましょう。

命令型の方法で結果の概要を取得する
ResultSummary resultSummary = imperativeClient
	.query("MATCH (m:Movie) where m.title = 'Aeon Flux' DETACH DELETE m")
	.run(); (1)

SummaryCounters counters = resultSummary.counters();
System.out.println(counters.nodesDeleted() + " nodes have been deleted")
1 ここでは、クエリがすぐにトリガーされます。

マッピングパラメーター

クエリには名前付きパラメーター ($someName) を含めることができ、Neo4j クライアントを使用すると値をそれらに簡単にバインドできます。

クライアントは、すべてのパラメーターがバインドされているかどうか、値が多すぎるかどうかをチェックしません。それはドライバーに任せられます。ただし、クライアントはパラメーター名を 2 回使用することを禁止します。

Java ドライバーが変換せずに理解できる単純な型をバインドすることも、複雑なクラスをバインドすることもできます。複雑なクラスの場合は、このリストに示すようにバインダー関数を提供する必要があります。どの単純な型がサポートされているかを確認するには、ドライバーマニュアル (英語) を参照してください。

単純な型のマッピング
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", "Li.*");

Flux<Map<String, Object>> directorAndMovies = client
	.query(
		"MATCH (p:Person) - [:DIRECTED] -> (m:Movie {title: $title}), (p) - [:WROTE] -> (om:Movie) " +
			"WHERE p.name =~ $name " +
			"  AND p.born < $someDate.year " +
			"RETURN p, om"
	)
	.bind("The Matrix").to("title") (1)
	.bind(LocalDate.of(1979, 9, 21)).to("someDate")
	.bindAll(parameters) (2)
	.fetch()
	.all();
1 単純な型をバインドするための流れるような API があります。
2 あるいは、名前付きパラメーターのマップを介してパラメーターをバインドすることもできます。

SDN は多くの複雑なマッピングを実行し、クライアントから使用できるものと同じ API を使用します。

ドメイン型の例の自転車の所有者などの特定のドメインオブジェクトの Function<T, Map<String, Object>> を Neo4j クライアントに提供して、それらのドメインオブジェクトをドライバーが理解できるパラメーターにマッピングできます。

ドメイン型の例
public class Director {

    private final String name;

    private final List<Movie> movies;

    Director(String name, List<Movie> movies) {
        this.name = name;
        this.movies = new ArrayList<>(movies);
    }

    public String getName() {
        return name;
    }

    public List<Movie> getMovies() {
        return Collections.unmodifiableList(movies);
    }
}

public class Movie {

    private final String title;

    public Movie(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

マッピング関数は、ドメインオブジェクトをバインドするためのマッピング関数の使用が示すように、クエリ内で発生する可能性のあるすべての名前付きパラメーターを入力する必要があります。

ドメインオブジェクトをバインドするためのマッピング関数の使用
Director joseph = new Director("Joseph Kosinski",
        Arrays.asList(new Movie("Tron Legacy"), new Movie("Top Gun: Maverick")));

Mono<ResultSummary> summary = client
    .query(""
        + "MERGE (p:Person {name: $name}) "
        + "WITH p UNWIND $movies as movie "
        + "MERGE (m:Movie {title: movie}) "
        + "MERGE (p) - [o:DIRECTED] -> (m) "
    )
    .bind(joseph).with(director -> { (1)
        Map<String, Object> mappedValues = new HashMap<>();
        List<String> movies = director.getMovies().stream()
            .map(Movie::getTitle).collect(Collectors.toList());
        mappedValues.put("name", director.getName());
        mappedValues.put("movies", movies);
        return mappedValues;
    })
    .run();
1with メソッドでは、バインダー機能を指定できます。

結果オブジェクトの操作

どちらのクライアントもマップのコレクションまたは発行者 (Map<String, Object>) を返します。これらのマップは、クエリによって生成された可能性のあるレコードと正確に一致します。

さらに、独自の BiFunction<TypeSystem, Record, T> から fetchAs をプラグインして、ドメインオブジェクトを再現できます。

ドメインオブジェクトを読み取るためのマッピング関数の使用
Mono<Director> lily = client
    .query(""
        + " MATCH (p:Person {name: $name}) - [:DIRECTED] -> (m:Movie)"
        + "RETURN p, collect(m) as movies")
    .bind("Lilly Wachowski").to("name")
    .fetchAs(Director.class).mappedBy((TypeSystem t, Record record) -> {
        List<Movie> movies = record.get("movies")
            .asList(v -> new Movie((v.get("title").asString())));
        return new Director(record.get("name").asString(), movies);
    })
    .one();

TypeSystem は、基礎となる Java ドライバーがレコードを埋めるために使用した型へのアクセスを提供します。

ドメイン対応マッピング関数の使用

クエリの結果にアプリケーション内のエンティティ定義を持つノードが含まれることがわかっている場合は、注入可能な MappingContext を使用してマッピング関数を取得し、マッピング中にそれらの関数を適用できます。

既存のマッピング機能を使用する
BiFunction<TypeSystem, MapAccessor, Movie> mappingFunction = neo4jMappingContext.getRequiredMappingFunctionFor(Movie.class);
Mono<Director> lily = client
    .query(""
        + " MATCH (p:Person {name: $name}) - [:DIRECTED] -> (m:Movie)"
        + "RETURN p, collect(m) as movies")
    .bind("Lilly Wachowski").to("name")
    .fetchAs(Director.class).mappedBy((TypeSystem t, Record record) -> {
        List<Movie> movies = record.get("movies")
            .asList(movie -> mappingFunction.apply(t, movie));
        return new Director(record.get("name").asString(), movies);
    })
    .one();

マネージドトランザクションの使用中にドライバーと直接対話する

Neo4jClient または ReactiveNeo4jClient の独自の「クライアント」アプローチを望まない場合、または気に入らない場合は、データベースとのすべての対話をクライアントにコードに委譲させることができます。委譲後の対話は、クライアントの命令型バージョンとリアクティブ型バージョンでは若干異なります。

命令型バージョンは、コールバックとして Function<StatementRunner, Optional<T>> を受け取ります。空のオプションを返しても問題ありません。

データベース対話を命令型 StatementRunner に委譲する
Optional<Long> result = client
    .delegateTo((StatementRunner runner) -> {
        // Do as many interactions as you want
        long numberOfNodes = runner.run("MATCH (n) RETURN count(n) as cnt")
            .single().get("cnt").asLong();
        return Optional.of(numberOfNodes);
    })
    // .in("aDatabase") (1)
    .run();
1 ターゲットデータベースの選択で説明されているデータベースの選択はオプションです。

リアクティブバージョンは RxStatementRunner を受け取ります。

データベース対話をリアクティブ RxStatementRunner に委譲する
Mono<Integer> result = client
    .delegateTo((RxStatementRunner runner) ->
        Mono.from(runner.run("MATCH (n:Unused) DELETE n").summary())
            .map(ResultSummary::counters)
            .map(SummaryCounters::nodesDeleted))
    // .in("aDatabase") (1)
    .run();
1 ターゲットデータベースのオプションの選択。

データベース対話を命令型 StatementRunner に委譲するデータベース対話をリアクティブ RxStatementRunner に委譲するの両方で、ランナーの型は、このマニュアルのリーダーにわかりやすくするためにのみ記載されていることに注意してください。