跳到主要内容

Neo4j 客户端

DeepSeek V3 中英对照 Neo4jClient

Spring Data Neo4j 带有一个 Neo4j 客户端,它在 Neo4j 的 Java 驱动之上提供了一个薄层。

虽然原生的 Java 驱动是一个非常通用的工具,除了提供命令式和响应式版本外,还提供了异步 API,但它并不与 Spring 应用级事务集成。

SDN 通过使用习惯性客户端的理念,尽可能直接地使用驱动程序。

客户端有以下主要目标

  1. 集成到 Spring 的事务管理中,适用于命令式和响应式场景

  2. 必要时参与 JTA 事务

  3. 为命令式和响应式场景提供一致的 API

  4. 不增加任何映射开销

SDN 依赖于所有这些特性,并利用它们来实现其实体映射功能。

请查看 SDN 构建模块,了解命令式和反应式 Neo4 客户端在我们堆栈中的位置。

Neo4j Client 有两种形式:

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

相比之下,响应式版本将返回一个请求类型的发布者。只有在订阅发布者之后,才会与数据库进行交互并检索结果。发布者只能被订阅一次。

获取客户端实例

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

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

驱动程序只能针对 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://localhost:7687", AuthTokens.basic("neo4j", "secret"));

ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver);
}
}
java
备注

请确保在启用事务的情况下,客户端使用的驱动程序实例与提供 Neo4jTransactionManagerReactiveNeo4jTransactionManager 时使用的实例相同。如果使用另一个驱动程序实例,客户端将无法同步事务。

我们的 Spring Boot 启动器提供了一个即用型的 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();
java
  • 选择要在其中执行查询的目标数据库。

指定查询

与客户端的交互从查询开始。查询可以通过一个普通的 StringSupplier<String> 来定义。Supplier 将会尽可能晚地被求值,并且可以由任何查询构建器提供。

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

检索结果

如之前的列表所示,与客户端的交互总是以调用 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
java
  • 实际查询在这里通过订阅发布者来触发。

请花点时间比较这两个列表,并理解在实际查询触发时的区别。

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")
java
  • 这里查询会立即触发。

映射参数

查询可以包含命名参数($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();
java
  • 提供了一个用于绑定简单类型的流式 API。

  • 或者,参数可以通过命名参数的映射进行绑定。

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;
}
}
java

映射函数需要填充查询中可能出现的所有命名参数,如使用映射函数绑定领域对象所示:

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();
java
  • 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();
java

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();
java

在使用托管事务时直接与驱动程序交互

如果你不希望或不喜欢 Neo4jClientReactiveNeo4jClient 这种带有主观色彩的“客户端”方式,你可以让客户端将所有与数据库的交互委托给你的代码。委托后的交互在命令式和响应式版本的客户端中略有不同。

命令式版本接受一个 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();
java

响应式版本接收一个 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();
java
  • 目标数据库的可选选择。

请注意,在将数据库交互委托给命令式的 StatementRunner将数据库交互委托给响应式的 RxStatementRunner 中,runner 的类型仅被声明为向本手册的读者提供更清晰的说明。