Neo4jClient
Spring Data Neo4j 附带一个 Neo4j Client,它在 Neo4j 的 Java 驱动程序之上提供了一层精简的界面。 虽然 plain Java driver 是一款用途极其广泛的工具,除命令式和反应式版本外还提供了异步 API,但它并不与 Spring 应用程序级别的交易集成。 SDN 通过惯用客户机这个概念使用驱动程序,尽可能直接。 该客户机有以下主要目标
-
针对命令式和响应式两种场景,与 Springs 事务管理集成
-
必要时参与 JTA 事务
-
针对命令式和响应式两种场景,提供一致的 API
-
不添加任何映射开销
SDN 依赖于所有这些功能并利用它们来实现其实体映射功能。 查看 SDN building blocks,了解命令式和响应式 Neo4 客户端在我们堆栈中的位置。 Neo4j Client 有两种版本:
-
org.springframework.data.neo4j.core.Neo4jClient
-
org.springframework.data.neo4j.core.ReactiveNeo4jClient
尽管两个版本都提供使用同一词汇和语法的 API,但它们不兼容。两个版本具有相同的、流畅的 API,用于指定查询、绑定参数和提取结果。
Imperative or reactive?
与 Neo4j Client 的交互通常以调用以下内容结束:
-
fetch().one()
-
fetch().first()
-
fetch().all()
-
run()
命令式版本此时将与数据库交互并获取请求结果或摘要,将其包装在 Optional<>
或 Collection
中。
相反,反应式版本将返回所请求类型的发布器。在发布器被订阅之前,不会与数据库进行交互,也不会检索结果。发布器只能订阅一次。
Getting an instance of the client
与 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);
}
}
确保您对客户端使用和提供 |
我们的 Spring Boot starter 提供了一个适用于环境(命令式或反应式)的 Neo4j 客户端的即用型 bean,通常您不必配置自己的实例。
Usage
Selecting the target database
Neo4j 客户端已做好准备,可与 Neo4j 4.0 的多数据库功能一起使用。除非另行指定,否则客户端使用默认数据库。客户端的 fluent API 允许在声明要执行的查询后,仅指定一次目标数据库。[neo4j-client-reactive-selecting-the-target-database] 使用反应式客户端演示了这一点:
Flux<Map<String, Object>> allActors = client
.query("MATCH (p:Person) RETURN p")
.in("neo4j") (1)
.fetch()
.all();
1 | 选择将查询执行到的目标数据库。 |
Specifying queries
与客户端的交互以查询开始。查询可以用一个简单的 String
或 Supplier<String>
来定义。该供应商将在尽可能晚的时候进行评估,并且可以在任何查询生成器中提供。
Mono<Map<String, Object>> firstActor = client
.query(() -> "MATCH (p:Person) RETURN p")
.fetch()
.first();
Retrieving results
如上例所示,与客户端的交互始终以对 fetch
的调用结束,且应接收多少个结果。反应式和命令式客户端都提供
one()
-
Expect exactly one result from the query
first()
-
Expect results and return the first record
all()
-
Retrieve all records returned
命令式客户端分别返回 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 | 在这里,查询立即触发。 |
Mapping parameters
查询可以包含命名参数 ($someName
),Neo4j 客户端让绑定值变得很容易。
客户端不会检查全部参数是否已绑定或是否有太多值。该问题已留交给驱动程序。但是,客户端会阻止您重复使用参数名称。 |
您可以只绑定 Java 驱动程序在没有转换的情况下理解的简单类型,或者绑定复杂类。对于复杂类,您需要提供一个粘合器函数,如 this listing所示。请查看 drivers manual,了解哪些简单类型受支持。
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。
您可以为任何给定的域对象(如 [neo4j-client-domain-example] 中的自行车所有者)提供一个 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;
}
}
映射函数必须填写查询中可能出现的全部命名参数,如 [neo4j-client-binder] 所示:
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 方法允许指定粘合函数。 |
Working with result objects
两个客户端均返回集合或映射的发布器 (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 驱动程序用于填充记录的类型。
Using domain-aware mapping functions
如果您知道查询的结果将包含在应用程序中具有实体定义的节点,则可以使用可注入的 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();
Interacting directly with the driver while using managed transactions
如果您不喜欢 Neo4jClient
或 ReactiveNeo4jClient
的固执“客户端”方法,您可以让客户端将与数据库的所有交互委派给您的代码。委派后的交互与命令式和反应式版本的客户端略有不同。
强制版本以回调的形式接收 Function<StatementRunner, Optional<T>>
。返回空可选值也没问题。
StatementRunner
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 | 如 Selecting the target database 所述的数据库选择是可选的。 |
响应版本接收 RxStatementRunner
。
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 | 目标数据库的可选选择。 |
请注意,在 [neo4j-client-imperative-delegating] 和 [neo4j-client-reactive-delegating] 中,仅指定了运行程序的类型,以便为该手册的读者提供更多清晰度。