Neo4jClient
Spring Data Neo4j 自带一个 Neo4j 客户端,为 Neo4j 的 Java 驱动程序提供了一层轻量级封装。
虽然 纯 Java 驱动程序 是一种非常灵活的工具,它不仅提供了异步 API,还支持命令式和响应式版本,但其无法与 Spring 应用层事务进行集成。
SDN 通过一种直观的客户端概念,尽可能直接地使用驱动程序。
客户端的主要目标如下
-
集成到 Spring 的事务管理中,适用于命令式和响应式场景
-
必要时参与 JTA 事务
-
为命令式和响应式场景提供一致的API
-
不要添加任何映射开销
SDN 依赖所有这些功能,并利用它们来实现其实体映射功能。
请查看 SDN 架构模块,了解我们架构中命令式和响应式 Neo4 客户端各自所处的位置。
Spring 框架有两种风格的 Neo4j 客户端:
-
org.springframework.data.neo4j.core.Neo4jClient -
org.springframework.data.neo4j.core.ReactiveNeo4jClient
虽然这两个版本都使用相同的词汇和语法提供API,但它们不向后兼容。</p><p>两个版本都提供了相同的流畅式API来指定查询、绑定参数和提取结果。
命令式或响应式?
与 Neo4j 客户端的交互通常以对的调用结束
-
fetch().one() -
fetch().first() -
fetch().all() -
run()
情报命令式版本将在本机与数据库进行交互,获取所需的查询结果或摘要,并将其打包到0 或1 中。
与数据库的交互和检索结果不会直到发布者被订阅才发生。发布者只能被订阅一次。
获取客户端实例
就像 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启动程序提供了一个准备好的bean,适合环境(命令式或响应式),通常您不需要配置自己的实例。
用法
选择目标数据库
新的多数据库功能大大增强了 Neo4j 的灵活性。我们可以用一组新方法来选择我们想要与哪个数据库进行交互。这些方法是在 Session 接口上添加的。这是非常干净的方式,因为它们不会破坏向后兼容性,并且在需要时很容易使用。
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 | 替代参数也可以通过一个命名参数的映射进行绑定。 |
SDN 执行许多复杂的映射,它使用与客户端相同的API。<br>
您可以向 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<String, Object>)。这些映射与查询可能产生的记录完全对应。
此外,您还可以插入自己的 BiFunction<TypeSystem, Record, T> 到 fetchAs 来重现您的领域对象。
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是可以的。
StatementRunnerOptional<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。
RxStatementRunnerMono<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两种情况下,仅声明了运行器的类型,以更清晰地向本手册读者提供信息。