Neo4jClient
Spring Data Neo4j 附带一个 Neo4j Client,它在 Neo4j 的 Java 驱动程序之上提供了一层精简的界面。
Spring Data Neo4j comes with a Neo4j Client, providing a thin layer on top of Neo4j’s Java driver.
虽然 plain Java driver 是一款用途极其广泛的工具,除命令式和反应式版本外还提供了异步 API,但它并不与 Spring 应用程序级别的交易集成。
While the plain Java driver is a very versatile tool providing an asynchronous API in addition to the imperative and reactive versions, it doesn’t integrate with Spring application level transactions.
SDN 通过惯用客户机这个概念使用驱动程序,尽可能直接。
SDN uses the driver through the concept of an idiomatic client as directly as possible.
该客户机有以下主要目标
The client has the following main goals
-
Integrate into Springs transaction management, for both imperative and reactive scenarios
-
Participate in JTA-Transactions if necessary
-
Provide a consistent API for both imperative and reactive scenarios
-
Don’t add any mapping overhead
SDN 依赖于所有这些功能并利用它们来实现其实体映射功能。
SDN relies on all those features and uses them to fulfill its entity mapping features.
查看 SDN building blocks,了解命令式和响应式 Neo4 客户端在我们堆栈中的位置。
Have a look at the SDN building blocks for where both the imperative and reactive Neo4 clients are positioned in our stack.
Neo4j Client 有两种版本:
The Neo4j Client comes in two flavors:
-
org.springframework.data.neo4j.core.Neo4jClient
-
org.springframework.data.neo4j.core.ReactiveNeo4jClient
尽管两个版本都提供使用同一词汇和语法的 API,但它们不兼容。两个版本具有相同的、流畅的 API,用于指定查询、绑定参数和提取结果。
While both versions provide an API using the same vocabulary and syntax, they are not API compatible. Both versions feature the same, fluent API to specify queries, bind parameters and extract results.
Imperative or reactive?
与 Neo4j Client 的交互通常以调用以下内容结束:
Interactions with a Neo4j Client usually ends with a call to
-
fetch().one()
-
fetch().first()
-
fetch().all()
-
run()
命令式版本此时将与数据库交互并获取请求结果或摘要,将其包装在 Optional<>
或 Collection
中。
The imperative version will interact at this moment with the database and get the requested results or summary, wrapped in an Optional<>
or a Collection
.
相反,反应式版本将返回所请求类型的发布器。在发布器被订阅之前,不会与数据库进行交互,也不会检索结果。发布器只能订阅一次。
The reactive version will in contrast return a publisher of the requested type. Interaction with the database and retrieval of the results will not happen until the publisher is subscribed to. The publisher can only be subscribed once.
Getting an instance of the client
与 SDN 中的大多数内容一样,两个客户端均依赖于已配置的驱动程序实例。
As with most things in SDN, both clients depend on a configured driver instance.
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 数据库打开一个反应式会话,并且对于任何较低版本都将失败并引发异常。
The driver can only open a reactive session against a 4.0 database and will fail with an exception on any lower version.
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);
}
}
确保您对客户端使用和提供 |
Make sure you use the same driver instance for the client as you used for providing a |
我们的 Spring Boot starter 提供了一个适用于环境(命令式或反应式)的 Neo4j 客户端的即用型 bean,通常您不必配置自己的实例。
Our Spring Boot starter provide a ready to use bean of the Neo4j Client that fits the environment (imperative or reactive) and you usually don’t have to configure your own instance.
Usage
Selecting the target database
Neo4j 客户端已做好准备,可与 Neo4j 4.0 的多数据库功能一起使用。除非另行指定,否则客户端使用默认数据库。客户端的 fluent API 允许在声明要执行的查询后,仅指定一次目标数据库。[neo4j-client-reactive-selecting-the-target-database] 使用反应式客户端演示了这一点:
The Neo4j client is well prepared to be used with the multidatabase features of Neo4j 4.0. The client uses the default database unless you specify otherwise. The fluent API of the client allows to specify the target database exactly once, after the declaration of the query to execute. [neo4j-client-reactive-selecting-the-target-database] demonstrates it with the reactive client:
Flux<Map<String, Object>> allActors = client
.query("MATCH (p:Person) RETURN p")
.in("neo4j") (1)
.fetch()
.all();
1 | Select the target database in which the query is to be executed. |
Specifying queries
与客户端的交互以查询开始。查询可以用一个简单的 String
或 Supplier<String>
来定义。该供应商将在尽可能晚的时候进行评估,并且可以在任何查询生成器中提供。
The interaction with the clients starts with a query.
A query can be defined by a plain String
or a Supplier<String>
.
The supplier will be evaluated as late as possible and can be provided by any query builder.
Mono<Map<String, Object>> firstActor = client
.query(() -> "MATCH (p:Person) RETURN p")
.fetch()
.first();
Retrieving results
如上例所示,与客户端的交互始终以对 fetch
的调用结束,且应接收多少个结果。反应式和命令式客户端都提供
As the previous listings shows, the interaction with the client always ends with a call to fetch
and how many results shall be received.
Both reactive and imperative client offer
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>
,后者仅在订阅后执行。
The imperative client returns Optional<T>
and Collection<T>
respectively, while the reactive client returns Mono<T>
and Flux<T>
, the later one being executed only if subscribed to.
如果您不希望从查询中得到任何结果,请在指定查询后使用 run()
。
If you don’t expect any results from your query, then use run()
after specifying the query.
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 | The actual query is triggered here by subscribing to the publisher. |
请花时间比较这两个清单,并了解在触发实际查询时的差异。
Please take a moment to compare both listings and understand the difference when the actual query is triggered.
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 | Here the query is immediately triggered. |
Mapping parameters
查询可以包含命名参数 ($someName
),Neo4j 客户端让绑定值变得很容易。
Queries can contain named parameters ($someName
) and the Neo4j client makes it easy to bind values to them.
客户端不会检查全部参数是否已绑定或是否有太多值。该问题已留交给驱动程序。但是,客户端会阻止您重复使用参数名称。 |
The client doesn’t check whether all parameters are bound or whether there are too many values. That is left to the driver. However, the client prevents you from using a parameter name twice. |
您可以只绑定 Java 驱动程序在没有转换的情况下理解的简单类型,或者绑定复杂类。对于复杂类,您需要提供一个粘合器函数,如 this listing所示。请查看 drivers manual,了解哪些简单类型受支持。
You can either bind simple types that the Java driver understands without conversion or complex classes. For complex classes you need to provide a binder function as shown in neo4j-client-binder. Please have a look at the drivers manual, to see which simple types are supported.
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 | There’s a fluent API for binding simple types. |
2 | Alternatively parameters can be bound via a map of named parameters. |
SDN 执行了许多复杂的映射,并且使用了与您在客户端中使用的相同 API。
SDN does a lot of complex mapping and it uses the same API that you can use from the client.
您可以为任何给定的域对象(如 [neo4j-client-domain-example] 中的自行车所有者)提供一个 Function<T, Map<String, Object>>
,以将这些域对象映射到驱动程序可以理解的参数。
You can provide a Function<T, Map<String, Object>>
for any given domain object like an owner of bicycles in [neo4j-client-domain-example]
to the Neo4j Client to map those domain objects to parameters the driver can understand.
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] 所示:
The mapping function has to fill in all named parameters that might occur in the query like [neo4j-client-binder] shows:
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 | The with method allows for specifying the binder function. |
Working with result objects
两个客户端均返回集合或映射的发布器 (Map<String, Object>
)。这些映射与查询可能生成的记录完全对应。
Both clients return collections or publishers of maps (Map<String, Object>
).
Those maps correspond exactly with the records that a query might have produced.
此外,您可以通过 fetchAs
插入自己的 BiFunction<TypeSystem, Record, T>
来重现您的域对象。
In addition, you can plug in your own BiFunction<TypeSystem, Record, T>
through fetchAs
to reproduce your domain object.
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 驱动程序用于填充记录的类型。
TypeSystem
gives access to the types the underlying Java driver used to fill the record.
Using domain-aware mapping functions
如果您知道查询的结果将包含在应用程序中具有实体定义的节点,则可以使用可注入的 MappingContext
检索它们的映射函数并在映射期间应用这些函数。
If you know that the result of the query will contain nodes that have entity definitions in your application,
you can use the injectable MappingContext
to retrieve their mapping functions and apply them during the mapping.
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
的固执“客户端”方法,您可以让客户端将与数据库的所有交互委派给您的代码。委派后的交互与命令式和反应式版本的客户端略有不同。
In case you don’t want or don’t like the opinionated "client" approach of the Neo4jClient
or the ReactiveNeo4jClient
, you can have the client delegate all interactions with the database to your code.
The interaction after the delegation is slightly different with the imperative and reactive versions of the client.
强制版本以回调的形式接收 Function<StatementRunner, Optional<T>>
。返回空可选值也没问题。
The imperative version takes in a Function<StatementRunner, Optional<T>>
as a callback.
Returning an empty optional is ok.
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 | The database selection as described in Selecting the target database is optional. |
响应版本接收 RxStatementRunner
。
The reactive version receives a 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 | Optional selection of the target database. |
请注意,在 [neo4j-client-imperative-delegating] 和 [neo4j-client-reactive-delegating] 中,仅指定了运行程序的类型,以便为该手册的读者提供更多清晰度。
Note that in both [neo4j-client-imperative-delegating] and [neo4j-client-reactive-delegating] the types of the runner have only been stated to provide more clarity to reader of this manual.