最新的稳定版本请使用 Spring Data Neo4j 7.3.1! |
最新的稳定版本请使用 Spring Data Neo4j 7.3.1! |
Spring Data Neo4j 带有一个 Neo4j 客户端,在 Neo4j 的 Java 驱动程序之上提供了一个薄层。
虽然普通的 Java 驱动程序是一个非常通用的工具,除了命令式和反应式版本之外,它还提供异步 API,但它不与 Spring 应用程序级事务集成。
SDN 尽可能直接地通过惯用客户端的概念使用驱动程序。
客户有以下主要目标
-
集成到 Springs 事务管理中,适用于命令式和被动式场景
-
如有必要,参与JTA-Transactions
-
为命令式和反应式方案提供一致的 API
-
不要添加任何映射开销
SDN 依赖于所有这些功能,并使用它们来实现其实体映射功能。
查看 SDN 构建块,了解命令式和响应式 Neo4 客户端在我们的堆栈中的位置。
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 starter 提供了一个适合环境(命令式或反应式)的 Neo4j 客户端的现成 bean,您通常不必配置自己的实例。
请确保对客户端使用与提供事务相同的驱动程序实例,或者在已启用事务的情况下。
如果使用驱动程序的另一个实例,客户端将无法同步事务。Neo4jTransactionManager ReactiveNeo4jTransactionManager |
用法
选择目标数据库
Neo4j 客户端已准备好与 Neo4j 4.0 的多数据库功能一起使用。除非另有指定,否则客户端将使用默认数据库。 客户端的 Fluent API 允许在声明要执行的查询后精确指定一次目标数据库。选择目标数据库可通过反应式客户端进行演示:
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 | 在这里,查询会立即触发。 |
映射参数
查询可以包含命名参数 (),而 Neo4j 客户端可以很容易地将值绑定到它们。$someName
客户端不会检查是否绑定了所有参数或是否存在太多值。 这是留给Drivers的。 但是,客户端会阻止您两次使用参数名称。 |
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 | 有一个用于绑定简单类型的 Fluent API。 |
2 | 或者,可以通过命名参数的映射来绑定参数。 |
SDN 执行许多复杂的映射,并且它使用可以从客户端使用的相同 API。
你可以为任何给定的域对象提供一个域类型示例中的自行车所有者,以将这些域对象映射到驱动程序可以理解的参数。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>
此外,您可以插入自己的 THROUGH 来重现您的域对象。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 驱动程序的类型的访问。
使用域感知映射函数
如果您知道查询结果将包含应用程序中具有实体定义的节点,
您可以使用 injectable 来检索它们的映射函数,并在映射过程中应用它们。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
命令式版本将 a 作为回调。
返回一个空的可选项是可以的。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 | 选择目标数据库中所述的数据库选择是可选的。 |
反应式版本收到 .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 | 可选选择目标数据库。 |
请注意,在委托数据库与命令式 StatementRunner
的交互和委托数据库与响应式 RxStatementRunner
的交互中,运行器的类型只是为了让本手册的读者更清楚。
1 | 选择要在其中执行查询的目标数据库。 |
1 | 此处通过订阅发布者触发实际查询。 |
1 | 在这里,查询会立即触发。 |
客户端不会检查是否绑定了所有参数或是否存在太多值。 这是留给Drivers的。 但是,客户端会阻止您两次使用参数名称。 |
1 | 有一个用于绑定简单类型的 Fluent API。 |
2 | 或者,可以通过命名参数的映射来绑定参数。 |
1 | 该方法允许指定活页夹函数。with |
1 | 选择目标数据库中所述的数据库选择是可选的。 |
1 | 可选选择目标数据库。 |