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

相反,响应式版本将返回请求类型的发布者。与数据库的交互以及结果的检索将不会发生,直到发布者被订阅。发布者只能被订阅一次。

获取客户端的实例

与 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://127.0.0.1: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://127.0.0.1:7687", AuthTokens.basic("neo4j", "secret"));

        ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver);
    }
}
如果您启用了事务,请确保您为客户端使用的驱动程序实例与您用于提供 Neo4jTransactionManagerReactiveNeo4jTransactionManager 的实例相同。如果您使用驱动程序的另一个实例,则客户端将无法同步事务。

我们的 Spring Boot 启动器提供了一个可立即使用的 Neo4j 客户端 Bean,该 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 或者,可以通过命名参数的映射绑定参数。

SDN 执行许多复杂的映射,它使用与您可以在客户端使用的相同的 API。

您可以为任何给定的域对象(如 域类型示例 中自行车的拥有者)提供一个 Function<T, Map<String, Object>> 到 Neo4j 客户端,以将这些域对象映射到驱动程序可以理解的参数。

域类型示例
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>)。这些映射与查询可能产生的记录完全对应。

此外,您可以通过 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>> 作为回调。返回空可选是可以的。

将数据库交互委托给命令式 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 中,运行程序的类型仅是为了向本手册的读者提供更多清晰度而说明的。