Neo4jClient
Spring Data Neo4j 附帶了一個 Neo4j 客戶端,在 Neo4j 的 Java 驅動之上提供了一個薄層。
儘管純粹的 Java 驅動是一個非常通用的工具,除了命令式和響應式版本外,還提供了非同步 API,但它不與 Spring 應用程式級別的事務整合。
SDN 透過一種符合慣例的客戶端概念儘可能直接地使用該驅動。
該客戶端的主要目標如下
-
整合到 Spring 的事務管理中,支援命令式和響應式場景
-
必要時參與 JTA 事務
-
為命令式和響應式場景提供一致的 API
-
不增加任何對映開銷
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 中的大多數事物一樣,兩個客戶端都依賴於配置好的驅動例項。
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 資料庫開啟響應式會話,對於任何較低版本將失敗並丟擲異常。
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);
}
}
如果您啟用了事務,請確保為客戶端使用的驅動例項與為提供 Neo4jTransactionManager 或 ReactiveNeo4jTransactionManager 所使用的驅動例項相同。如果您使用另一個驅動例項,客戶端將無法同步事務。 |
我們的 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 | 選擇要執行查詢的目標資料庫。 |
指定查詢
與客戶端的互動從查詢開始。查詢可以透過純文字 String
或 Supplier<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 客戶端使其易於將值繫結到這些引數。
客戶端不檢查是否所有引數都已繫結,也不檢查值是否過多。這留給驅動處理。但是,客戶端會阻止您重複使用同一個引數名。 |
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();
在使用託管事務時直接與驅動互動
如果您不希望或不喜歡 Neo4jClient
或 ReactiveNeo4jClient
的有主見的“客戶端”方法,您可以讓客戶端將所有與資料庫的互動委託給您的程式碼。委託後的互動在客戶端的命令式版本和響應式版本之間略有不同。
命令式版本接受一個 Function<StatementRunner, Optional<T>>
作為回撥。返回一個空的 Optional 是可以的。
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
。
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 的型別,以便為本手冊的讀者提供更多清晰度。