跳到主要内容
版本:8.1.1

Neo4jClient

Doubao 中英对照 Neo4jClient

Spring Data Neo4j 自带一个 Neo4j 客户端,在 Neo4j Java 驱动之上提供了一层薄封装。

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

SDN 尽可能直接地通过惯用客户端(idiomatic client)的概念来使用驱动。

该客户端有以下主要目标

  1. 集成到 Spring 的事务管理中,同时支持命令式和响应式场景

  2. 必要时参与 JTA 事务

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

  4. 不增加任何映射开销

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

请参阅SDN 构建块,了解命令式和响应式 Neo4j 客户端在我们技术栈中的位置。

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。

相比之下,响应式版本会返回一个请求类型的发布器。与数据库的交互和结果的获取在订阅该发布器之前不会发生。该发布器只能被订阅一次。

获取客户端实例​

与 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);
}
}

驱动只能针对 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);
}
}
备注

如果您已启用事务,请确保客户端使用的驱动实例与您提供 Neo4jTransactionManager 或 ReactiveNeo4jTransactionManager 时所用的实例相同。如果使用另一个驱动实例,客户端将无法同步事务。

我们的 Spring Boot 起步依赖会提供一个开箱即用的 Neo4j 客户端 bean,它会适配当前环境(命令式或响应式),因此您通常无需自行配置实例。

用法​

选择目标数据库​

Neo4j 客户端已做好充分准备,可配合 Neo4j 4.0 的多数据库特性使用。除非另行指定,否则客户端使用默认数据库。客户端的流式 API 允许在声明要执行的查询之后,恰好指定一次目标数据库。选择目标数据库以响应式客户端为例演示了这一点:

Flux<Map<String, Object>> allActors = client
.query("MATCH (p:Person) RETURN p")
.in("neo4j") 1
.fetch()
.all();
  • 选择要执行查询的目标数据库。

指定查询​

与客户端的交互从一条查询开始。查询可以用普通 String 或 Supplier<String> 定义。该 supplier 会尽可能延迟求值,并可由任何查询构建器提供。

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
  • 实际查询在这里通过订阅发布器触发。

请花一点时间对比这两个代码清单,体会实际查询触发时机的差异。

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

映射参数​

查询可以包含命名参数($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();
  • 绑定简单类型有对应的流式 API。

  • 或者,参数也可以通过命名参数 Map 绑定。

SDN 内部进行大量复杂映射时,使用的正是您可以从客户端使用的同一套 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();
  • 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();

在使用受管事务的同时直接与驱动交互​

如果您不想要、或不喜欢 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();

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

请注意,在将数据库交互委托给命令式 StatementRunner和将数据库交互委托给响应式 RxStatementRunner这两处,runner 的类型只是为了让本手册的读者更清楚而特意写出。