Neo4j 客户端
Spring Data Neo4j 带有一个 Neo4j 客户端,它在 Neo4j 的 Java 驱动之上提供了一个薄层。
虽然原生的 Java 驱动是一个非常通用的工具,除了提供命令式和响应式版本外,还提供了异步 API,但它并不与 Spring 应用级事务集成。
SDN 通过使用习惯性客户端的理念,尽可能直接地使用驱动程序。
客户端有以下主要目标
-
集成到 Spring 的事务管理中,适用于命令式和响应式场景
-
必要时参与 JTA 事务
-
为命令式和响应式场景提供一致的 API
-
不增加任何映射开销
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);
}
}
驱动程序只能针对 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 Client 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 客户端使得将这些参数与值绑定变得非常容易。
客户端不会检查是否所有参数都已绑定,也不会检查是否有过多的值。这些检查由驱动程序负责。然而,客户端会阻止你重复使用同一个参数名。
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。
或者,参数可以通过命名参数的映射进行绑定。
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();
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 的类型仅被声明为向本手册的读者提供更清晰的说明。