此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Data Neo4j 7.4.4!spring-doc.cadn.net.cn

Neo4j客户端

Spring Data Neo4j 带有一个 Neo4j 客户端,在 Neo4j 的 Java 驱动程序之上提供了一个薄层。spring-doc.cadn.net.cn

虽然普通 Java 驱动程序是一个非常通用的工具,除了命令式和反应式版本之外,还提供异步 API,但它并不与 Spring 应用程序级事务集成。spring-doc.cadn.net.cn

SDN 通过惯用客户端的概念尽可能直接地使用驱动程序。spring-doc.cadn.net.cn

客户有以下主要目标spring-doc.cadn.net.cn

  1. 集成到 Springs 事务管理中,适用于命令式和反应式场景spring-doc.cadn.net.cn

  2. 必要时参与 JTA 交易spring-doc.cadn.net.cn

  3. 为命令式和反应式方案提供一致的 APIspring-doc.cadn.net.cn

  4. 不添加任何映射开销spring-doc.cadn.net.cn

SDN 依赖于所有这些功能,并使用它们来实现其实体映射功能。spring-doc.cadn.net.cn

查看 SDN 构建块,了解命令式和反应式 Neo4 客户端在我们的堆栈中的位置。spring-doc.cadn.net.cn

Neo4j 客户端有两种类型:spring-doc.cadn.net.cn

虽然这两个版本都提供了使用相同词汇和语法的 API,但它们不兼容 API。 两个版本都具有相同的 Fluent API 来指定查询、绑定参数和提取结果。spring-doc.cadn.net.cn

命令式还是反应式?

与 Neo4j 客户端的交互通常以调用spring-doc.cadn.net.cn

此时,命令式版本将与数据库交互,并获取请求的结果或摘要,并包装在Optional<>Collection.spring-doc.cadn.net.cn

相反,反应式版本将返回请求类型的发布者。 在订阅发布服务器之前,不会发生与数据库的交互和结果的检索。 发布者只能订阅一次。spring-doc.cadn.net.cn

获取客户端的实例

与 SDN 中的大多数内容一样,两个客户端都依赖于配置的驱动程序实例。spring-doc.cadn.net.cn

创建命令式 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);
    }
}

The driver can only open a reactive session against a 4.0 database and will fail with an exception on any lower version.spring-doc.cadn.net.cn

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);
    }
}
Make sure you use the same driver instance for the client as you used for providing a Neo4jTransactionManager or ReactiveNeo4jTransactionManager in case you have enabled transactions. The client won’t be able to synchronize transactions if you use another instance of a driver.

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.spring-doc.cadn.net.cn

Usage

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. Selecting the target database demonstrates it with the reactive client:spring-doc.cadn.net.cn

Selecting the target database
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

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.spring-doc.cadn.net.cn

Specifying a query
Mono<Map<String, Object>> firstActor = client
	.query(() -> "MATCH (p:Person) RETURN p")
	.fetch()
	.first();

Retrieving results

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 offerspring-doc.cadn.net.cn

one()

Expect exactly one result from the queryspring-doc.cadn.net.cn

first()

Expect results and return the first recordspring-doc.cadn.net.cn

all()

Retrieve all records returnedspring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

If you don’t expect any results from your query, then use run() after specifying the query.spring-doc.cadn.net.cn

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 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.spring-doc.cadn.net.cn

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 Here the query is immediately triggered.

Mapping parameters

Queries can contain named parameters ($someName) and the Neo4j client makes it easy to bind values to them.spring-doc.cadn.net.cn

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.

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 this listing. Please have a look at the drivers manual, to see which simple types are supported.spring-doc.cadn.net.cn

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 There’s a fluent API for binding simple types.
2 Alternatively parameters can be bound via a map of named parameters.

SDN does a lot of complex mapping and it uses the same API that you can use from the client.spring-doc.cadn.net.cn

You can provide a Function<T, Map<String, Object>> for any given domain object like an owner of bicycles in Example of a domain type to the Neo4j Client to map those domain objects to parameters the driver can understand.spring-doc.cadn.net.cn

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;
    }
}

The mapping function has to fill in all named parameters that might occur in the query like Using a mapping function for binding domain objects shows:spring-doc.cadn.net.cn

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 The with method allows for specifying the binder function.

Working with result objects

Both clients return collections or publishers of maps (Map<String, Object>). Those maps correspond exactly with the records that a query might have produced.spring-doc.cadn.net.cn

In addition, you can plug in your own BiFunction<TypeSystem, Record, T> through fetchAs to reproduce your domain object.spring-doc.cadn.net.cn

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 gives access to the types the underlying Java driver used to fill the record.spring-doc.cadn.net.cn

Using domain-aware mapping functions

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.spring-doc.cadn.net.cn

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

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.spring-doc.cadn.net.cn

The imperative version takes in a Function<StatementRunner, Optional<T>> as a callback. Returning an empty optional is ok.spring-doc.cadn.net.cn

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 The database selection as described in Selecting the target database is optional.

The reactive version receives a RxStatementRunner.spring-doc.cadn.net.cn

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 Optional selection of the target database.

Note that in both Delegate database interaction to an imperative StatementRunner and Delegate database interaction to a reactive RxStatementRunner the types of the runner have only been stated to provide more clarity to reader of this manual.spring-doc.cadn.net.cn