Neo4jClient

Spring Data Neo4j 附帶了一個 Neo4j 客戶端,在 Neo4j 的 Java 驅動之上提供了一個薄層。

儘管純粹的 Java 驅動是一個非常通用的工具,除了命令式和響應式版本外,還提供了非同步 API,但它不與 Spring 應用程式級別的事務整合。

SDN 透過一種符合慣例的客戶端概念儘可能直接地使用該驅動。

該客戶端的主要目標如下

  1. 整合到 Spring 的事務管理中,支援命令式和響應式場景

  2. 必要時參與 JTA 事務

  3. 為命令式和響應式場景提供一致的 API

  4. 不增加任何對映開銷

SDN 依賴於所有這些特性,並利用它們來實現其實體對映功能。

請檢視 SDN 組成部分,瞭解命令式和響應式 Neo4 客戶端在我們的技術棧中的位置。

Neo4j 客戶端有兩種形式

  • 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 中。

相比之下,響應式版本將返回請求型別的釋出者 (publisher)。與資料庫的互動和結果的檢索直到訂閱釋出者時才會發生。釋出者只能被訂閱一次。

獲取客戶端例項

與 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://: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://:7687", AuthTokens.basic("neo4j", "secret"));

        ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver);
    }
}
如果您啟用了事務,請確保為客戶端使用的驅動例項與為提供 Neo4jTransactionManagerReactiveNeo4jTransactionManager 所使用的驅動例項相同。如果您使用另一個驅動例項,客戶端將無法同步事務。

我們的 Spring Boot Starter 提供了適合環境(命令式或響應式)的、即用型的 Neo4j 客戶端 Bean,通常您無需自行配置例項。

用法

選擇目標資料庫

Neo4j 客戶端已為配合 Neo4j 4.0 的多資料庫特性做好充分準備。除非另有指定,客戶端預設使用預設資料庫。客戶端的流式 API 允許在宣告要執行的查詢之後,精確指定一次目標資料庫。選擇目標資料庫 使用響應式客戶端進行了演示

選擇目標資料庫
Flux<Map<String, Object>> allActors = client
	.query("MATCH (p:Person) RETURN p")
	.in("neo4j") (1)
	.fetch()
	.all();
1 選擇要執行查詢的目標資料庫。

指定查詢

與客戶端的互動從查詢開始。查詢可以透過純文字 StringSupplier<String> 定義。供應商將在儘可能晚的時候被評估,並可以由任何查詢構建器提供。

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

檢索結果

如前面的列表中所示,與客戶端的互動總是以呼叫 fetch 以及要接收多少結果的方法結束。響應式和命令式客戶端都提供了

one()

期望查詢返回正好一個結果

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 客戶端使其易於將值繫結到這些引數。

客戶端不檢查是否所有引數都已繫結,也不檢查值是否過多。這留給驅動處理。但是,客戶端會阻止您重複使用同一個引數名。

您可以繫結 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 或者,引數可以透過命名引數的 map 進行繫結。

SDN 進行了許多複雜的對映,並且它使用的 API 與您可以從客戶端使用的 API 相同。

您可以為任何給定的領域物件(例如 領域型別示例 中的腳踏車所有者)向 Neo4j 客戶端提供一個 Function<T, Map<String, Object>>,以便將這些領域物件對映到驅動可以理解的引數。

領域型別示例
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();
1 with 方法允許指定繫結函式。

處理結果物件

兩個客戶端都返回 map 的集合或釋出者(Map<String, Object>)。這些 map 精確對應於查詢可能產生的記錄。

此外,您可以透過 fetchAs 插入您自己的 BiFunction<TypeSystem, Record, T> 來重現您的領域物件。

使用對映函式讀取領域物件
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();

在使用託管事務時直接與驅動互動

如果您不希望或不喜歡 Neo4jClientReactiveNeo4jClient 的有主見的“客戶端”方法,您可以讓客戶端將所有與資料庫的互動委託給您的程式碼。委託後的互動在客戶端的命令式版本和響應式版本之間略有不同。

命令式版本接受一個 Function<StatementRunner, Optional<T>> 作為回撥。返回一個空的 Optional 是可以的。

將資料庫互動委託給命令式 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 中,只說明瞭 runner 的型別,以便為本手冊的讀者提供更多清晰度。