Neo4jClient

Spring Data Neo4j 隨附 Neo4j Client,在 Neo4j 的 Java 驅動程式之上提供一個薄層。

雖然純 Java 驅動程式是一個非常通用的工具,除了命令式和反應式版本外,還提供非同步 API,但它並未與 Spring 應用程式層級交易整合。

SDN 盡可能直接地透過慣用的用戶端概念來使用驅動程式。

用戶端具有以下主要目標

  1. 整合到 Spring 的交易管理中,適用於命令式和反應式情境

  2. 必要時參與 JTA 交易

  3. 為命令式和反應式情境提供一致的 API

  4. 不增加任何映射額外負荷

SDN 依賴所有這些功能,並使用它們來實現其實體映射功能。

請查看SDN 建構區塊,了解命令式和反應式 Neo4j 用戶端在我們的堆疊中的位置。

Neo4j Client 有兩種版本

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

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

雖然兩個版本都使用相同的詞彙和語法提供 API,但它們的 API 並不相容。兩個版本都具有相同的流暢 API,可指定查詢、繫結參數和擷取結果。

命令式或反應式?

與 Neo4j Client 的互動通常以呼叫結束

  • fetch().one()

  • fetch().first()

  • fetch().all()

  • run()

命令式版本將在此時與資料庫互動,並取得請求的結果或摘要,並包裝在 Optional<>Collection 中。

相反地,反應式版本將傳回請求類型的發布者。與資料庫的互動和結果的擷取將在訂閱發布者之後才會發生。發布者只能訂閱一次。

取得用戶端的實例

與 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);
    }
}
如果您已啟用交易,請確保為用戶端使用與您為提供 Neo4jTransactionManagerReactiveNeo4jTransactionManager 所使用的相同的驅動程式實例。如果您使用驅動程式的另一個實例,則用戶端將無法同步交易。

我們的 Spring Boot Starter 提供適用於環境(命令式或反應式)的 Neo4j Client 的現成可用 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 或者,可以透過具名參數的地圖來繫結參數。

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

您可以為任何給定的網域物件(例如網域類型範例中的自行車擁有者)提供 Function<T, Map<String, Object>> 給 Neo4j Client,以將這些網域物件映射到驅動程式可以理解的參數。

網域類型範例
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<String, Object>)。這些地圖與查詢可能產生的記錄完全對應。

此外,您可以透過 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>> 作為回呼。傳回空的可選值是可以的。

將資料庫互動委派給命令式 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 中,執行器的類型僅為了向本手冊的讀者提供更多清晰度而陳述。