Neo4jClient

Spring Data Neo4j 附带一个 Neo4j Client,它在 Neo4j 的 Java 驱动程序之上提供了一层精简的界面。 虽然 plain Java driver 是一款用途极其广泛的工具,除命令式和反应式版本外还提供了异步 API,但它并不与 Spring 应用程序级别的交易集成。 SDN 通过惯用客户机这个概念使用驱动程序,尽可能直接。 该客户机有以下主要目标

  1. 针对命令式和响应式两种场景,与 Springs 事务管理集成

  2. 必要时参与 JTA 事务

  3. 针对命令式和响应式两种场景,提供一致的 API

  4. 不添加任何映射开销

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 中的大多数内容一样,两个客户端均依赖于已配置的驱动程序实例。

Creating an instance of the imperative Neo4j client
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 数据库打开一个反应式会话,并且对于任何较低版本都将失败并引发异常。

Creating an instance of the reactive Neo4j client
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,通常您不必配置自己的实例。

Usage

Selecting the target database

Neo4j 客户端已做好准备,可与 Neo4j 4.0 的多数据库功能一起使用。除非另行指定,否则客户端使用默认数据库。客户端的 fluent API 允许在声明要执行的查询后,仅指定一次目标数据库。[neo4j-client-reactive-selecting-the-target-database] 使用反应式客户端演示了这一点:

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

Specifying queries

与客户端的交互以查询开始。查询可以用一个简单的 StringSupplier<String> 来定义。该供应商将在尽可能晚的时候进行评估,并且可以在任何查询生成器中提供。

Specifying a query
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()

Retrieving result summaries in a reactive way
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 真正的查询在这里通过订阅发布者触发。

请花时间比较这两个清单,并了解在触发实际查询时的差异。

Retrieving result summaries in an imperative way
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,了解哪些简单类型受支持。

Mapping simple types
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>>,以将这些域对象映射到驱动程序可以理解的参数。

Example of a domain type
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] 所示:

Using a mapping function for binding domain objects
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> 来重现您的域对象。

Using a mapping function for reading domain objects
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 检索它们的映射函数并在映射期间应用这些函数。

Using an existing mapping function
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

如果您不喜欢 Neo4jClientReactiveNeo4jClient 的固执“客户端”方法,您可以让客户端将与数据库的所有交互委派给您的代码。委派后的交互与命令式和反应式版本的客户端略有不同。

强制版本以回调的形式接收 Function<StatementRunner, Optional<T>>。返回空可选值也没问题。

Delegate database interaction to an imperative 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

Delegate database interaction to a reactive 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] 中,仅指定了运行程序的类型,以便为该手册的读者提供更多清晰度。