Neo4jClient

Spring Data Neo4j 附带了一个 Neo4j 客户端,在 Neo4j 的 Java 驱动之上提供了一个薄层。

尽管纯粹的 Java 驱动是一个非常通用的工具,除了命令式和响应式版本外,还提供了异步 API,但它不与 Spring 应用程序级别的事务集成。

SDN 通过一种符合惯例的客户端概念尽可能直接地使用该驱动。

该客户端的主要目标如下

  1. 集成到 Spring 的事务管理中,支持命令式和响应式场景

  2. 必要时参与 JTA 事务

  3. 为命令式和响应式场景提供一致的 API

  4. 不增加任何映射开销

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 中。

相比之下,响应式版本将返回请求类型的发布者 (publisher)。与数据库的交互和结果的检索直到订阅发布者时才会发生。发布者只能被订阅一次。

获取客户端实例

与 SDN 中的大多数事物一样,两个客户端都依赖于配置好的驱动实例。

创建命令式 Neo4j 客户端实例
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 数据库打开响应式会话,对于任何较低版本将失败并抛出异常。

创建响应式 Neo4j 客户端实例
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);
    }
}
如果您启用了事务,请确保为客户端使用的驱动实例与为提供 Neo4jTransactionManagerReactiveNeo4jTransactionManager 所使用的驱动实例相同。如果您使用另一个驱动实例,客户端将无法同步事务。

我们的 Spring Boot Starter 提供了适合环境(命令式或响应式)的、即用型的 Neo4j 客户端 Bean,通常您无需自行配置实例。

用法

选择目标数据库

Neo4j 客户端已为配合 Neo4j 4.0 的多数据库特性做好充分准备。除非另有指定,客户端默认使用默认数据库。客户端的流式 API 允许在声明要执行的查询之后,精确指定一次目标数据库。选择目标数据库 使用响应式客户端进行了演示

选择目标数据库
Flux<Map<String, Object>> allActors = client
	.query("MATCH (p:Person) RETURN p")
	.in("neo4j") (1)
	.fetch()
	.all();
1 选择要执行查询的目标数据库。

指定查询

与客户端的交互从查询开始。查询可以通过纯文本 StringSupplier<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 客户端使其易于将值绑定到这些参数。

客户端不检查是否所有参数都已绑定,也不检查值是否过多。这留给驱动处理。但是,客户端会阻止您重复使用同一个参数名。

您可以绑定 Java 驱动无需转换就能理解的简单类型,或者绑定复杂类。对于复杂类,您需要提供一个绑定函数,如 本列表 所示。请查看 驱动手册,了解支持哪些简单类型。

映射简单类型
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 或者,参数可以通过命名参数的 map 进行绑定。

SDN 进行了许多复杂的映射,并且它使用的 API 与您可以从客户端使用的 API 相同。

您可以为任何给定的领域对象(例如 领域类型示例 中的自行车所有者)向 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 的集合或发布者(Map<String, Object>)。这些 map 精确对应于查询可能产生的记录。

此外,您可以通过 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();

在使用托管事务时直接与驱动交互

如果您不希望或不喜欢 Neo4jClientReactiveNeo4jClient 的有主见的“客户端”方法,您可以让客户端将所有与数据库的交互委托给您的代码。委托后的交互在客户端的命令式版本和响应式版本之间略有不同。

命令式版本接受一个 Function<StatementRunner, Optional<T>> 作为回调。返回一个空的 Optional 是可以的。

将数据库交互委托给命令式 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 中,只说明了 runner 的类型,以便为本手册的读者提供更多清晰度。