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

常见问题

Neo4j-OGM 是一个对象图映射库,主要被以前版本的 Spring Data Neo4j 用作其后端,用于将节点和关系映射到域对象中的繁重工作。 当前的 SDN 不需要也不支持 Neo4j-OGM。 SDN 专门使用 Spring Data 的映射上下文来扫描类和构建元模型。spring-doc.cadn.net.cn

虽然这将 SDN 固定到 Spring 生态系统中,但它有几个优点,其中包括 CPU 和内存使用方面的占用空间更小,尤其是 Spring 映射上下文的所有功能。spring-doc.cadn.net.cn

为什么我应该使用 SDN 来支持 SDN+OGM

SDN 具有 SDN+OGM 中不存在的几个功能,特别是spring-doc.cadn.net.cn

SDN 是否支持通过 HTTP 连接到 Neo4j?

SDN是否支持嵌入式Neo4j?

嵌入式 Neo4j 有多个方面:spring-doc.cadn.net.cn

SDN 是否为您的应用程序提供嵌入式实例?

SDN 是否直接与嵌入式实例交互?

不。 嵌入式数据库通常由org.neo4j.graphdb.GraphDatabaseService并且没有开箱即用的 Bolt 连接器。spring-doc.cadn.net.cn

然而,SDN 可以与 Neo4j 的测试工具很好地配合使用,该测试工具专门用于替代真实数据库。 对 Neo4j 3.5、4.x 和 5.x 测试工具的支持是通过驱动程序的 Spring Boot Starters实现的。 看看相应的模块org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure.spring-doc.cadn.net.cn

可以使用哪种 Neo4j Java 驱动程序以及如何使用?

SDN 依赖于 Neo4j Java 驱动程序。 每个 SDN 版本都使用与其发布时可用的最新 Neo4j 兼容的 Neo4j Java 驱动程序版本 释放。 虽然 Neo4j Java 驱动程序的补丁版本通常是直接替代品,但 SDN 确保即使是次要版本 是可互换的,因为它会检查方法的存在与否,或者在必要时进行接口更改。spring-doc.cadn.net.cn

因此,您可以将任何 4.x Neo4j Java 驱动程序与任何 SDN 6.x 版本一起使用, 以及任何具有任何 SDN 7.x 版本的任何 5.x Neo4j 驱动程序。spring-doc.cadn.net.cn

使用 Spring Boot

如今,Spring boot 部署是最有可能的基于 Spring Data 的应用程序的部署。请使用 Spring Boots 依赖管理以更改驱动程序版本,如下所示:spring-doc.cadn.net.cn

从 Maven (pom.xml) 更改驱动程序版本
<properties>
  <neo4j-java-driver.version>5.4.0</neo4j-java-driver.version>
</properties>
从 Gradle (gradle.properties) 更改驱动程序版本
neo4j-java-driver.version = 5.4.0

没有 Spring Boot

如果没有 Spring Boot,您只需手动声明依赖项。对于 Maven,我们建议使用<dependencyManagement />像这样的部分:spring-doc.cadn.net.cn

更改驱动程序版本,无需 Spring Boot from Maven (pom.xml)
<dependencyManagement>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
        <version>5.4.0</version>
    </dependency>
</dependencyManagement>

Neo4j 4 支持多个数据库 - 如何使用它们?

您可以静态配置数据库名称,也可以运行自己的数据库名称提供程序。 请记住,SDN 不会为您创建数据库。 您可以借助迁移工具,当然也可以预先使用简单的脚本来完成此作。spring-doc.cadn.net.cn

静态配置

配置要在 Spring Boot 配置中使用的数据库名称,如下所示(当然,相同的属性适用于 YML 或基于环境的配置,并应用了 Spring Boot 的约定):spring-doc.cadn.net.cn

spring.data.neo4j.database = yourDatabase

采用该配置后,SDN 存储库的所有实例(响应式和命令式)生成的所有查询以及ReactiveNeo4jTemplate分别Neo4jTemplate将针对数据库执行yourDatabase.spring-doc.cadn.net.cn

动态配置

提供类型为Neo4jDatabaseNameProviderReactiveDatabaseSelectionProvider取决于您的 Spring 应用程序的类型。spring-doc.cadn.net.cn

例如,该 bean 可以使用 Spring 的安全上下文来检索租户。 以下是使用 Spring Security 保护的命令式应用程序的工作示例:spring-doc.cadn.net.cn

Neo4jConfig.java
import org.neo4j.springframework.data.core.DatabaseSelection;
import org.neo4j.springframework.data.core.DatabaseSelectionProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;

@Configuration
public class Neo4jConfig {

	@Bean
	DatabaseSelectionProvider databaseSelectionProvider() {

		return () -> Optional.ofNullable(SecurityContextHolder.getContext()).map(SecurityContext::getAuthentication)
				.filter(Authentication::isAuthenticated).map(Authentication::getPrincipal).map(User.class::cast)
				.map(User::getUsername).map(DatabaseSelection::byName).orElseGet(DatabaseSelection::undecided);
	}
}
请注意,不要将从一个数据库检索到的实体与另一个数据库混淆。 为每个新事务请求数据库名称,因此在两次调用之间更改数据库名称时,最终的实体可能比预期的要少或多。 或者更糟糕的是,您可能不可避免地将错误的实体存储在错误的数据库中。

Spring Boot Neo4j 健康指示器针对默认数据库,如何更改它?

Spring Boot 带有命令式和响应式 Neo4j 健康指标。两种变体都能够检测到多个org.neo4j.driver.Driver并在应用程序上下文中提供 对每个实例的整体健康状况的贡献。 但是,Neo4j 驱动程序确实连接到服务器,而不是该服务器内的特定数据库。 Spring Boot 能够在没有 Spring Data Neo4j 的情况下配置驱动程序,并作为要使用哪个数据库的信息 绑定到 Spring Data Neo4j,此信息不适用于内置健康指标。spring-doc.cadn.net.cn

在许多部署方案中,这很可能不是问题。 但是,如果配置的数据库用户至少没有对默认数据库的访问权限,则运行状况检查将失败。spring-doc.cadn.net.cn

这可以通过了解数据库选择的自定义 Neo4j 健康贡献者来缓解。spring-doc.cadn.net.cn

命令式变体

import java.util.Optional;

import org.neo4j.driver.Driver;
import org.neo4j.driver.Result;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.summary.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.util.StringUtils;

public class DatabaseSelectionAwareNeo4jHealthIndicator extends AbstractHealthIndicator {

    private final Driver driver;

    private final DatabaseSelectionProvider databaseSelectionProvider;

    public DatabaseSelectionAwareNeo4jHealthIndicator(
        Driver driver, DatabaseSelectionProvider databaseSelectionProvider
    ) {
        this.driver = driver;
        this.databaseSelectionProvider = databaseSelectionProvider;
    }

    @Override
    protected void doHealthCheck(Health.Builder builder) {
        try {
            SessionConfig sessionConfig = Optional
                .ofNullable(databaseSelectionProvider.getDatabaseSelection())
                .filter(databaseSelection -> databaseSelection != DatabaseSelection.undecided())
                .map(DatabaseSelection::getValue)
                .map(v -> SessionConfig.builder().withDatabase(v).build())
                .orElseGet(SessionConfig::defaultConfig);

            class Tuple {
                String edition;
                ResultSummary resultSummary;

                Tuple(String edition, ResultSummary resultSummary) {
                    this.edition = edition;
                    this.resultSummary = resultSummary;
                }
            }

            String query =
                "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
            Tuple health = driver.session(sessionConfig)
                .writeTransaction(tx -> {
                    Result result = tx.run(query);
                    String edition = result.single().get("edition").asString();
                    return new Tuple(edition, result.consume());
                });

            addHealthDetails(builder, health.edition, health.resultSummary);
        } catch (Exception ex) {
            builder.down().withException(ex);
        }
    }

    static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
        ServerInfo serverInfo = resultSummary.server();
        builder.up()
            .withDetail(
                "server", serverInfo.version() + "@" + serverInfo.address())
            .withDetail("edition", edition);
        DatabaseInfo databaseInfo = resultSummary.database();
        if (StringUtils.hasText(databaseInfo.name())) {
            builder.withDetail("database", databaseInfo.name());
        }
    }
}

这将使用可用的数据库选择来运行与 Boot 运行的相同查询,以检查连接是否正常。 使用以下配置来应用它:spring-doc.cadn.net.cn

import java.util.Map;

import org.neo4j.driver.Driver;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;

@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {

    @Bean (1)
    DatabaseSelectionAwareNeo4jHealthIndicator databaseSelectionAwareNeo4jHealthIndicator(
        Driver driver, DatabaseSelectionProvider databaseSelectionProvider
    ) {
        return new DatabaseSelectionAwareNeo4jHealthIndicator(driver, databaseSelectionProvider);
    }

    @Bean (2)
    HealthContributor neo4jHealthIndicator(
        Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators) {
        return CompositeHealthContributor.fromMap(customNeo4jHealthIndicators);
    }

    @Bean (3)
    InitializingBean healthContributorRegistryCleaner(
        HealthContributorRegistry healthContributorRegistry,
        Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators
    ) {
        return () -> customNeo4jHealthIndicators.keySet()
            .stream()
            .map(HealthContributorNameFactory.INSTANCE)
            .forEach(healthContributorRegistry::unregisterContributor);
    }
}
1 如果您有多个驱动程序和数据库选择提供程序,则需要为每个组合创建一个指标
2 这确保所有这些指标都归入 Neo4j 下,取代默认的 Neo4j 健康指标
3 这可以防止单个参与者直接显示在运行状况终结点中

反应性变体

响应式变体基本相同,使用响应式类型和相应的响应式基础设施类:spring-doc.cadn.net.cn

import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;

import org.neo4j.driver.Driver;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.reactivestreams.RxResult;
import org.neo4j.driver.reactivestreams.RxSession;
import org.neo4j.driver.summary.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import org.reactivestreams.Publisher;
import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.util.StringUtils;

public final class DatabaseSelectionAwareNeo4jReactiveHealthIndicator
    extends AbstractReactiveHealthIndicator {

    private final Driver driver;

    private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;

    public DatabaseSelectionAwareNeo4jReactiveHealthIndicator(
        Driver driver,
        ReactiveDatabaseSelectionProvider databaseSelectionProvider
    ) {
        this.driver = driver;
        this.databaseSelectionProvider = databaseSelectionProvider;
    }

    @Override
    protected Mono<Health> doHealthCheck(Health.Builder builder) {
        String query =
            "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
        return databaseSelectionProvider.getDatabaseSelection()
            .map(databaseSelection -> databaseSelection == DatabaseSelection.undecided() ?
                SessionConfig.defaultConfig() :
                SessionConfig.builder().withDatabase(databaseSelection.getValue()).build()
            )
            .flatMap(sessionConfig ->
                Mono.usingWhen(
                    Mono.fromSupplier(() -> driver.rxSession(sessionConfig)),
                    s -> {
                        Publisher<Tuple2<String, ResultSummary>> f = s.readTransaction(tx -> {
                            RxResult result = tx.run(query);
                            return Mono.from(result.records())
                                .map((record) -> record.get("edition").asString())
                                .zipWhen((edition) -> Mono.from(result.consume()));
                        });
                        return Mono.fromDirect(f);
                    },
                    RxSession::close
                )
            ).map((result) -> {
                addHealthDetails(builder, result.getT1(), result.getT2());
                return builder.build();
            });
    }

    static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
        ServerInfo serverInfo = resultSummary.server();
        builder.up()
            .withDetail(
                "server", serverInfo.version() + "@" + serverInfo.address())
            .withDetail("edition", edition);
        DatabaseInfo databaseInfo = resultSummary.database();
        if (StringUtils.hasText(databaseInfo.name())) {
            builder.withDetail("database", databaseInfo.name());
        }
    }
}

当然,还有配置的反应变体。它需要两个不同的注册表清理器,就像 Spring Boot 一样 包装现有的反应指示器以与非反应性执行器端点一起使用。spring-doc.cadn.net.cn

import java.util.Map;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
import org.springframework.boot.actuate.health.HealthContributorNameFactory;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.ReactiveHealthContributor;
import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {

    @Bean
    ReactiveHealthContributor neo4jHealthIndicator(
        Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
        return CompositeReactiveHealthContributor.fromMap(customNeo4jHealthIndicators);
    }

    @Bean
    InitializingBean healthContributorRegistryCleaner(HealthContributorRegistry healthContributorRegistry,
        Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
        return () -> customNeo4jHealthIndicators.keySet()
            .stream()
            .map(HealthContributorNameFactory.INSTANCE)
            .forEach(healthContributorRegistry::unregisterContributor);
    }

    @Bean
    InitializingBean reactiveHealthContributorRegistryCleaner(
        ReactiveHealthContributorRegistry healthContributorRegistry,
        Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
        return () -> customNeo4jHealthIndicators.keySet()
            .stream()
            .map(HealthContributorNameFactory.INSTANCE)
            .forEach(healthContributorRegistry::unregisterContributor);
    }
}

Neo4j 4.4+ 支持模拟不同用户 - 如何使用它们?

在大型多租户环境中,用户模拟特别有趣,其中物理连接(或技术连接) 用户可以模拟多个租户。根据您的设置,这将大大减少所需的物理驱动程序实例数量。spring-doc.cadn.net.cn

该功能需要服务器端的 Neo4j Enterprise 4.4+ 和客户端的 4.4+ 驱动程序(org.neo4j.driver:neo4j-java-driver:4.4.0或更高)。spring-doc.cadn.net.cn

对于命令式和响应式版本,您需要提供一个UserSelectionProvider分别是ReactiveUserSelectionProvider. 需要将相同的实例传递给Neo4ClientNeo4jTransactionManager分别是它们的反应性变体。spring-doc.cadn.net.cn

无引导命令式和响应式配置中,您只需要提供一个类型:spring-doc.cadn.net.cn

用户选择提供者 Bean
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.UserSelectionProvider;

public class CustomConfig {

    @Bean
    public UserSelectionProvider getUserSelectionProvider() {
        return () -> UserSelection.impersonate("someUser");
    }
}

在典型的 Spring Boot 场景中,此功能需要更多的工作,因为 Boot 也支持没有该功能的 SDN 版本。因此,给定用户选择提供程序 bean 中的 bean,您需要完全自定义客户端和事务管理器:spring-doc.cadn.net.cn

Spring Boot 的必要定制
import org.neo4j.driver.Driver;

import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.UserSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;

import org.springframework.transaction.PlatformTransactionManager;

public class CustomConfig {

    @Bean
    public Neo4jClient neo4jClient(
        Driver driver,
        DatabaseSelectionProvider databaseSelectionProvider,
        UserSelectionProvider userSelectionProvider
    ) {

        return Neo4jClient.with(driver)
            .withDatabaseSelectionProvider(databaseSelectionProvider)
            .withUserSelectionProvider(userSelectionProvider)
            .build();
	}

    @Bean
    public PlatformTransactionManager transactionManager(
        Driver driver,
        DatabaseSelectionProvider databaseSelectionProvider,
        UserSelectionProvider userSelectionProvider
    ) {

        return Neo4jTransactionManager
            .with(driver)
            .withDatabaseSelectionProvider(databaseSelectionProvider)
            .withUserSelectionProvider(userSelectionProvider)
            .build();
	}
}

使用 Spring Data Neo4j 中的 Neo4j 集群实例

以下问题适用于 Neo4j AuraDB 以及 Neo4j 本地集群实例。spring-doc.cadn.net.cn

我是否需要特定的配置,以便事务与 Neo4j 因果集群无缝协作?

不,你没有。 SDN 在内部使用 Neo4j Causal Cluster 书签,无需您进行任何配置。 同一线程或同一响应流中的事务将能够按照您的预期读取它们之前更改的值。spring-doc.cadn.net.cn

对 Neo4j 集群使用只读事务重要吗?

是的,它是。 Neo4j集群架构是一种因果集群架构,它区分了主服务器和辅助服务器。 主服务器要么是单实例,要么是核心实例。它们都可以回答读写作。 写入作从核心实例传播到集群内的只读副本或更一般的跟随者。 这些关注者是辅助服务器。 辅助服务器不响应写入作。spring-doc.cadn.net.cn

在标准部署方案中,群集中将有一些核心实例和许多只读副本。 因此,请务必将作或查询标记为只读,以缩放集群,使领导者 永远不会不堪重负,查询会尽可能多地传播到只读副本。spring-doc.cadn.net.cn

Spring Data Neo4j 和底层 Java 驱动程序都不进行 Cypher 解析,并且两个构建块都假定 写入作。做出此决定是为了支持所有开箱即用的作。如果 stack 默认为只读,堆栈最终可能会向只读副本发送写入查询并失败 在执行它们时。spring-doc.cadn.net.cn

findById,findAllById,findAll默认情况下,预定义的存在方法被标记为只读。

下面介绍了一些选项:spring-doc.cadn.net.cn

将整个存储库设为只读
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
interface PersonRepository extends Neo4jRepository<Person, Long> {
}
将选定的存储库方法设为只读
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.transaction.annotation.Transactional;

interface PersonRepository extends Neo4jRepository<Person, Long> {

  @Transactional(readOnly = true)
  Person findOneByName(String name); (1)

  @Transactional(readOnly = true)
  @Query("""
    CALL apoc.search.nodeAll('{Person: "name",Movie: ["title","tagline"]}','contains','her')
    YIELD node AS n RETURN n""")
  Person findByCustomQuery(); (2)
}
1 为什么这个只读不是默认的?虽然它适用于上面的派生查找器(我们实际上知道它是只读的), 我们经常看到用户添加自定义@Query并通过MERGE构建 这当然是写入作。
2 自定义过程可以做各种各样的事情,目前没有办法为我们检查只读与写入。
编排从服务对存储库的调用
import java.util.Optional;

import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.transaction.annotation.Transactional;

interface PersonRepository extends Neo4jRepository<Person, Long> {
}

interface MovieRepository extends Neo4jRepository<Movie, Long> {
  List<Movie> findByLikedByPersonName(String name);
}

public class PersonService {

  private final PersonRepository personRepository;
  private final MovieRepository movieRepository;

  public PersonService(PersonRepository personRepository,
        MovieRepository movieRepository) {
    this.personRepository = personRepository;
    this.movieRepository = movieRepository;
  }

  @Transactional(readOnly = true)
  public Optional<PersonDetails> getPerson(Long id) { (1)
    return this.repository.findById(id)
      .map(person -> {
        var movies = this.movieRepository
          .findByLikedByPersonName(person.getName());
        return new PersonDetails(person, movies);
            });
    }
}
1 在这里,对多个存储库的多个调用被包装在一个单读事务中。
使用弹簧TransactionTemplate在私有服务方法和/或使用 Neo4j 客户端
import java.util.Collection;

import org.neo4j.driver.types.Node;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

public class PersonService {

  private final TransactionTemplate readOnlyTx;

  private final Neo4jClient neo4jClient;

  public PersonService(PlatformTransactionManager transactionManager, Neo4jClient neo4jClient) {

    this.readOnlyTx = new TransactionTemplate(transactionManager, (1)
        new TransactionDefinition() {
          @Override public boolean isReadOnly() {
            return true;
          }
        }
    );
    this.neo4jClient = neo4jClient;
  }

  void internalOperation() { (2)

    Collection<Node> nodes = this.readOnlyTx.execute(state -> {
      return neo4jClient.query("MATCH (n) RETURN n").fetchAs(Node.class) (3)
          .mappedBy((types, record) -> record.get(0).asNode())
          .all();
    });
  }
}
1 创建TransactionTemplate具有您需要的特征。 当然,这也可以是一个全局 bean。
2 使用事务模板的原因一:声明性事务不起作用 在包私有或私有方法中,也不在内部方法调用中(想象另一个方法 在此服务中调用internalOperation)由于其性质是与 Aspect 一起实现的 和代理。
3 Neo4jClient是 SDN 提供的固定实用程序。它不能被注释,但它与 Spring 集成。 因此,它为您提供了使用纯驱动程序所做的一切,无需自动映射,并且 交易。它还服从声明性事务。

我可以检索最新的书签或为事务管理器播种吗?

正如书签管理中简要提到的,无需配置任何与书签相关的内容。 但是,检索 SDN 事务系统从数据库接收的最新书签可能很有用。 您可以添加一个@Bean喜欢BookmarkCapture为此,请执行以下作:spring-doc.cadn.net.cn

BookmarkCapture.java
import java.util.Set;

import org.neo4j.driver.Bookmark;
import org.springframework.context.ApplicationListener;

public final class BookmarkCapture
    implements ApplicationListener<Neo4jBookmarksUpdatedEvent> {

    @Override
    public void onApplicationEvent(Neo4jBookmarksUpdatedEvent event) {
        // We make sure that this event is called only once,
        // the thread safe application of those bookmarks is up to your system.
        Set<Bookmark> latestBookmarks = event.getBookmarks();
    }
}

要为事务系统设定种子,需要自定义事务管理器,如下所示:spring-doc.cadn.net.cn

BookmarkSeedingConfig.java
import java.util.Set;
import java.util.function.Supplier;

import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class BookmarkSeedingConfig {

    @Bean
    public PlatformTransactionManager transactionManager(
            Driver driver, DatabaseSelectionProvider databaseNameProvider) { (1)

        Supplier<Set<Bookmark>> bookmarkSupplier = () -> { (2)
            Bookmark a = null;
            Bookmark b = null;
            return Set.of(a, b);
        };

        Neo4jBookmarkManager bookmarkManager =
            Neo4jBookmarkManager.create(bookmarkSupplier); (3)
        return new Neo4jTransactionManager(
            driver, databaseNameProvider, bookmarkManager); (4)
    }
}
1 让 Spring 注入这些
2 该提供商可以是任何包含要带入系统的最新书签的
3 使用它创建书签管理器
4 将其传递给自定义事务管理器
无需执行上述任何作,除非您的应用程序需要访问或提供 这些数据。如果有疑问,也不要这样做。

我可以禁用书签管理吗?

我们提供了一个 Noop 书签管理器,可以有效地禁用书签管理。spring-doc.cadn.net.cn

使用此书签管理器的风险由您自行承担,它将通过删除所有书签来有效地禁用任何书签管理 书签,从不提供任何书签。在集群中,您将面临遇到过时读取的高风险。在单个 例如,它很可能不会产生任何影响。

+ 在集群中,这可能只是一种明智的方法,前提是您可以容忍过时的读取并且没有危险 覆盖旧数据。spring-doc.cadn.net.cn

以下配置创建了书签管理器的“noop”变体,该变体将从相关类中选取。spring-doc.cadn.net.cn

BookmarksDisabledConfig.java
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;

@Configuration
public class BookmarksDisabledConfig {

    @Bean
    public Neo4jBookmarkManager neo4jBookmarkManager() {

        return Neo4jBookmarkManager.noop();
    }
}

您可以配置Neo4jTransactionManager/Neo4jClientReactiveNeo4jTransactionManager/ReactiveNeo4jClient单独配置,但我们建议仅在已针对特定数据库选择需求配置它们时才这样做。spring-doc.cadn.net.cn

我需要使用 Neo4j 特定的注释吗?

不。 您可以自由使用以下等效的 Spring Data 注释:spring-doc.cadn.net.cn

SDN 特定注释 Spring Data 通用注解 目的 差异

org.springframework.data.neo4j.core.schema.Idspring-doc.cadn.net.cn

org.springframework.data.annotation.Idspring-doc.cadn.net.cn

将带注释的属性标记为唯一 ID。spring-doc.cadn.net.cn

特定注释没有其他功能。spring-doc.cadn.net.cn

org.springframework.data.neo4j.core.schema.Nodespring-doc.cadn.net.cn

org.springframework.data.annotation.Persistentspring-doc.cadn.net.cn

将类标记为持久实体。spring-doc.cadn.net.cn

@Node允许自定义标签spring-doc.cadn.net.cn

如何使用分配的 ID?

只需使用@Id没有@GeneratedValue并通过构造函数参数或 setter 或 wither 填充您的 id 属性。 有关寻找良好 ID 的一些一般性说明,请参阅此博客文章spring-doc.cadn.net.cn

如何使用外部生成的 ID?

我们提供接口org.springframework.data.neo4j.core.schema.IdGenerator. 以您想要的任何方式实现它,并按如下方式配置您的实现:spring-doc.cadn.net.cn

ThingWithGeneratedId.java
@Node
public class ThingWithGeneratedId {

	@Id @GeneratedValue(TestSequenceGenerator.class)
	private String theId;
}

如果将类名传递给@GeneratedValue,则此类必须具有无参数的默认构造函数。 但是,您也可以使用字符串:spring-doc.cadn.net.cn

ThingWithIdGeneratedByBean.java
@Node
public class ThingWithIdGeneratedByBean {

	@Id @GeneratedValue(generatorRef = "idGeneratingBean")
	private String theId;
}

有了这个,idGeneratingBean指的是 Spring 上下文中的 bean。 这对于序列生成可能很有用。spring-doc.cadn.net.cn

id 的非最终字段不需要 setter。

我必须为每个域类创建存储库吗?

不。 查看 SDN 构建基块并找到Neo4jTemplateReactiveNeo4jTemplate.spring-doc.cadn.net.cn

这些模板知道你的域,并提供检索、写入和计数实体所需的所有基本 CRUD 方法。spring-doc.cadn.net.cn

这是我们带有命令式模板的规范电影示例:spring-doc.cadn.net.cn

TemplateExampleTest.java
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Collections;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
import org.springframework.data.neo4j.documentation.domain.Roles;

@DataNeo4jTest
public class TemplateExampleTest {

	@Test
	void shouldSaveAndReadEntities(@Autowired Neo4jTemplate neo4jTemplate) {

		MovieEntity movie = new MovieEntity("The Love Bug",
				"A movie that follows the adventures of Herbie, Herbie's driver, "
						+ "Jim Douglas (Dean Jones), and Jim's love interest, " + "Carole Bennett (Michele Lee)");

		Roles roles1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
		Roles roles2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
		movie.getActorsAndRoles().add(roles1);
		movie.getActorsAndRoles().add(roles2);

		MovieEntity result = neo4jTemplate.save(movie);
		assertThat(result.getActorsAndRoles()).allSatisfy(relationship -> assertThat(relationship.getId()).isNotNull());

		Optional<PersonEntity> person = neo4jTemplate.findById("Dean Jones", PersonEntity.class);
		assertThat(person).map(PersonEntity::getBorn).hasValue(1931);

		assertThat(neo4jTemplate.count(PersonEntity.class)).isEqualTo(2L);
	}

}

这是响应式版本,为了简洁起见省略了设置:spring-doc.cadn.net.cn

ReactiveTemplateExampleTest.java
import reactor.test.StepVerifier;

import java.util.Collections;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
import org.springframework.data.neo4j.documentation.domain.Roles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@DataNeo4jTest
class ReactiveTemplateExampleTest {

	@Container private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");

	@DynamicPropertySource
	static void neo4jProperties(DynamicPropertyRegistry registry) {
		registry.add("org.neo4j.driver.uri", neo4jContainer::getBoltUrl);
		registry.add("org.neo4j.driver.authentication.username", () -> "neo4j");
		registry.add("org.neo4j.driver.authentication.password", neo4jContainer::getAdminPassword);
	}

	@Test
	void shouldSaveAndReadEntities(@Autowired ReactiveNeo4jTemplate neo4jTemplate) {

		MovieEntity movie = new MovieEntity("The Love Bug",
				"A movie that follows the adventures of Herbie, Herbie's driver, Jim Douglas (Dean Jones), and Jim's love interest, Carole Bennett (Michele Lee)");

		Roles role1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
		Roles role2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
		movie.getActorsAndRoles().add(role1);
		movie.getActorsAndRoles().add(role2);

		StepVerifier.create(neo4jTemplate.save(movie)).expectNextCount(1L).verifyComplete();

		StepVerifier.create(neo4jTemplate.findById("Dean Jones", PersonEntity.class).map(PersonEntity::getBorn))
				.expectNext(1931).verifyComplete();

		StepVerifier.create(neo4jTemplate.count(PersonEntity.class)).expectNext(2L).verifyComplete();
	}
}

请注意,这两个示例都使用@DataNeo4jTest来自 Spring Boot。spring-doc.cadn.net.cn

如何将自定义查询与存储库方法一起使用Page<T>Slice<T>?

虽然您不必提供任何其他东西,但Pageable作为派生查找器方法的参数 返回一个Page<T>Slice<T>,则必须准备自定义查询来处理可分页。页面和切片可让您大致了解所需内容。spring-doc.cadn.net.cn

页面和切片
import org.springframework.data.domain.Pageable;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;

public interface MyPersonRepository extends Neo4jRepository<Person, Long> {

    Page<Person> findByName(String name, Pageable pageable); (1)

    @Query(""
        + "MATCH (n:Person) WHERE n.name = $name RETURN n "
        + "ORDER BY n.name ASC SKIP $skip LIMIT $limit"
    )
    Slice<Person> findSliceByName(String name, Pageable pageable); (2)

    @Query(
    	value = ""
            + "MATCH (n:Person) WHERE n.name = $name RETURN n "
            + "ORDER BY n.name ASC SKIP $skip LIMIT $limit",
        countQuery = ""
            + "MATCH (n:Person) WHERE n.name = $name RETURN count(n)"
    )
    Page<Person> findPageByName(String name, Pageable pageable); (3)
}
1 为你创建查询的派生查找器方法。 它处理Pageable给你的。 您应该使用排序的可分页对象。
2 此方法使用@Query以定义自定义查询。它返回一个Slice<Person>. 切片不知道总页数,因此自定义查询 不需要专用的计数查询。SDN 将通知你它估计了下一个切片。 Cypher 模板必须同时发现两者$skip$limitCypher 参数。 如果省略它们,SDN 将发出警告。这可能不符合您的期望。 此外,Pageable应该是未排序的,你应该提供一个稳定的顺序。 我们不会使用可分页对象中的排序信息。
3 此方法返回一个页面。页面知道确切的总页数。 因此,必须指定其他计数查询。 第二种方法的所有其他限制都适用。

我可以映射命名路径吗?

一系列连接的节点和关系在 Neo4j 中称为“路径”。 Cypher 允许使用标识符命名路径,例如:spring-doc.cadn.net.cn

p = (a)-[*3..5]->(b)

或者如臭名昭著的电影图所示,其中包括以下路径(在这种情况下,是两个演员之间的最短路径之一):spring-doc.cadn.net.cn

“培根”距离
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
RETURN p

它看起来像这样:spring-doc.cadn.net.cn

image$培根距离

我们发现 3 个节点标记为Vertex和 2 个标记为Movie.两者都可以使用自定义查询进行映射。 假设两者都有一个节点实体VertexMovie以及Actor照顾好关系:spring-doc.cadn.net.cn

“标准”电影图域模型
@Node
public final class Person {

	@Id @GeneratedValue
	private final Long id;

	private final String name;

	private Integer born;

	@Relationship("REVIEWED")
	private List<Movie> reviewed = new ArrayList<>();
}

@RelationshipProperties
public final class Actor {

	@RelationshipId
	private final Long id;

	@TargetNode
	private final Person person;

	private final List<String> roles;
}

@Node
public final class Movie {

	@Id
	private final String title;

	@Property("tagline")
	private final String description;

	@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
	private final List<Actor> actors;
}

使用查询时,如类型为Vertex喜欢这个spring-doc.cadn.net.cn

interface PeopleRepository extends Neo4jRepository<Person, Long> {
    @Query(""
        + "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
        + "RETURN p"
    )
    List<Person> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
}

它将从路径中检索所有人员并绘制地图。 如果路径上存在关系类型,例如REVIEWED这些也存在于域中,这些 将从路径中相应地填充。spring-doc.cadn.net.cn

使用从基于路径的查询水合的节点来保存数据时,请特别小心。 如果不是所有关系都已水合,则数据将丢失。

反之亦然。相同的查询可以与Movie实体。 然后它只会填充电影。 以下列表显示了如何执行此作,以及如何使用其他数据扩充查询 在路上找不到。该数据用于正确填充缺失的关系(在这种情况下,所有参与者)spring-doc.cadn.net.cn

interface MovieRepository extends Neo4jRepository<Movie, String> {

    @Query(""
        + "MATCH p=shortestPath(\n"
        + "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
        + "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
        + "UNWIND x AS m\n"
        + "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
        + "RETURN p, collect(r), collect(d)"
    )
    List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
}

查询返回路径以及收集的所有关系和相关节点,以便电影实体完全水合。spring-doc.cadn.net.cn

路径映射适用于单个路径以及路径的多个记录(由allShortestPath函数。spring-doc.cadn.net.cn

命名路径可以有效地用于填充和返回多个根节点,请参阅 appendix/custom-queries.adoc#custom-query.paths

@Query使用自定义查询的唯一方法?

@Query不是运行自定义查询的唯一方法。 在自定义查询完全填充域的情况下,注释很舒服。 请记住,SDN 假定映射的域模型是真实的。 这意味着,如果您通过@Query仅部分填充模型,您就有使用相同模型的危险 对象以写回数据,这最终将擦除或覆盖您在查询中未考虑的数据。spring-doc.cadn.net.cn

因此,请将存储库和声明式方法与@Query在所有情况下,如果结果的形状与您的域相似 model 或您确定不使用部分映射的模型来写入命令。spring-doc.cadn.net.cn

有哪些替代方案?spring-doc.cadn.net.cn

  • 投影可能已经足以在图表上塑造您的视图:它们可用于定义 以显式方式获取属性和相关实体的深度:通过对它们进行建模。spring-doc.cadn.net.cn

  • 如果您的目标是仅使查询的条件动态化,请查看QuerydslPredicateExecutor但尤其是我们自己的变体,CypherdslConditionExecutor.这两个 mixin 都允许将条件添加到 我们为您创建的完整查询。因此,您将拥有与自定义条件一起完全填充域。 当然,您的条件必须与我们生成的内容相匹配。在此处查找根节点、相关节点等的名称。spring-doc.cadn.net.cn

  • 通过CypherdslStatementExecutorReactiveCypherdslStatementExecutor. Cypher-DSL 注定要创建动态查询。最后,无论如何,这都是 SDN 在幕后使用的。相应的 mixin 既适用于存储库本身的域类型,也适用于投影(用于添加的 mixin 条件不然)。spring-doc.cadn.net.cn

如果您认为可以通过部分动态查询或完全动态查询与投影一起解决问题, 现在请跳回有关 Spring Data Neo4j Mixins 的章节。spring-doc.cadn.net.cn

否则,请阅读两件事:自定义存储库将我们在 SDN 中提供的抽象级别分段。spring-doc.cadn.net.cn

为什么现在谈论自定义存储库片段?spring-doc.cadn.net.cn

  • 您可能遇到更复杂的情况,其中需要多个动态查询,但查询仍然属于 概念上在存储库中,而不是在服务层中spring-doc.cadn.net.cn

  • 自定义查询返回一个图形形状的结果,该结果不太适合您的域模型 因此,自定义查询也应附带自定义映射spring-doc.cadn.net.cn

  • 您需要与驱动程序交互,即不应通过对象映射的批量加载。spring-doc.cadn.net.cn

假设以下存储库声明基本上聚合了一个基本存储库和 3 个片段:spring-doc.cadn.net.cn

由多个片段组成的存储库
import org.springframework.data.neo4j.repository.Neo4jRepository;

public interface MovieRepository extends Neo4jRepository<MovieEntity, String>,
        DomainResults,
        NonDomainResults,
        LowlevelInteractions {
}

存储库包含电影,如入门部分所示。spring-doc.cadn.net.cn

存储库从中扩展的附加接口 (DomainResults,NonDomainResultsLowlevelInteractions) 是解决上述所有问题的片段。spring-doc.cadn.net.cn

使用复杂的动态自定义查询,但仍返回域类型

片段DomainResults声明一个附加方法findMoviesAlongShortestPath:spring-doc.cadn.net.cn

DomainResults 片段
interface DomainResults {

    @Transactional(readOnly = true)
    List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to);
}

此方法用@Transactional(readOnly = true)以表明读者可以回答。 它不能由 SDN 派生,但需要自定义查询。 此自定义查询由该接口的一个实现提供。 实现具有相同的名称和后缀Impl:spring-doc.cadn.net.cn

使用 Neo4jTemplate 的片段实现
import static org.neo4j.cypherdsl.core.Cypher.anyNode;
import static org.neo4j.cypherdsl.core.Cypher.listWith;
import static org.neo4j.cypherdsl.core.Cypher.name;
import static org.neo4j.cypherdsl.core.Cypher.node;
import static org.neo4j.cypherdsl.core.Cypher.parameter;
import static org.neo4j.cypherdsl.core.Cypher.shortestPath;

import org.neo4j.cypherdsl.core.Cypher;

class DomainResultsImpl implements DomainResults {

    private final Neo4jTemplate neo4jTemplate; (1)

    DomainResultsImpl(Neo4jTemplate neo4jTemplate) {
        this.neo4jTemplate = neo4jTemplate;
    }

    @Override
    public List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to) {

        var p1 = node("Person").withProperties("name", parameter("person1"));
        var p2 = node("Person").withProperties("name", parameter("person2"));
        var shortestPath = shortestPath("p").definedBy(
                p1.relationshipBetween(p2).unbounded()
        );
        var p = shortestPath.getRequiredSymbolicName();
        var statement = Cypher.match(shortestPath)
                .with(p, listWith(name("n"))
                        .in(Cypher.nodes(shortestPath))
                        .where(anyNode().named("n").hasLabels("Movie")).returning().as("mn")
                )
                .unwind(name("mn")).as("m")
                .with(p, name("m"))
                .match(node("Person").named("d")
                        .relationshipTo(anyNode("m"), "DIRECTED").named("r")
                )
                .returning(p, Cypher.collect(name("r")), Cypher.collect(name("d")))
                .build();

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("person1", from.getName());
        parameters.put("person2", to.getName());
        return neo4jTemplate.findAll(statement, parameters, MovieEntity.class); (2)
    }
}
1 Neo4jTemplate由运行时通过DomainResultsImpl. 无需@Autowired.
2 Cypher-DSL 用于构建复杂的语句(与路径映射中显示的几乎相同。)该语句可以直接传递给模板。

该模板也具有基于字符串的查询的重载,因此您也可以将查询写为 String。这里的重要要点是:spring-doc.cadn.net.cn

使用自定义查询和自定义映射

通常,自定义查询指示自定义结果。所有这些结果是否应映射为@Node? 当然不是! 很多时候,这些对象代表读取命令并且不打算用作写入命令。SDN 也不太可能或不想不映射 Cypher 可能的所有内容。但是,它确实提供了几个钩子来运行您自己的映射:在Neo4jClient. 使用 SDN 的好处Neo4jClient在驱动器上:spring-doc.cadn.net.cn

声明片段与之前完全相同:spring-doc.cadn.net.cn

声明非域类型结果的片段
interface NonDomainResults {

    class Result { (1)
        public final String name;

        public final String typeOfRelation;

        Result(String name, String typeOfRelation) {
            this.name = name;
            this.typeOfRelation = typeOfRelation;
        }
    }

    @Transactional(readOnly = true)
    Collection<Result> findRelationsToMovie(MovieEntity movie); (2)
}
1 这是一个虚构的非域结果。真实世界的查询结果可能看起来更复杂。
2 此片段添加的方法。同样,该方法使用 Spring 的@Transactional

如果没有该片段的实现,启动将失败,所以这里是:spring-doc.cadn.net.cn

使用 Neo4jClient 的片段实现
class NonDomainResultsImpl implements NonDomainResults {

    private final Neo4jClient neo4jClient; (1)

    NonDomainResultsImpl(Neo4jClient neo4jClient) {
        this.neo4jClient = neo4jClient;
    }

    @Override
    public Collection<Result> findRelationsToMovie(MovieEntity movie) {
        return this.neo4jClient
                .query(""
                       + "MATCH (people:Person)-[relatedTo]-(:Movie {title: $title}) "
                       + "RETURN people.name AS name, "
                       + "       Type(relatedTo) as typeOfRelation"
                ) (2)
                .bind(movie.getTitle()).to("title") (3)
                .fetchAs(Result.class) (4)
                .mappedBy((typeSystem, record) -> new Result(record.get("name").asString(),
                        record.get("typeOfRelation").asString())) (5)
                .all(); (6)
    }
}
1 在这里,我们使用Neo4jClient,由基础结构提供。
2 客户端仅接受字符串,但在渲染成字符串时仍然可以使用 Cypher-DSL
3 将一个值绑定到命名参数。还有一个重载来绑定整个参数映射
4 这是您想要的结果类型
5 最后,mappedBy方法,公开一个Record对于结果中的每个条目,如果需要,还可以输入驱动程序类型系统。 这是您挂接自定义映射的 API

整个查询在 Spring 事务的上下文中运行,在本例中为只读事务。spring-doc.cadn.net.cn

低级交互

有时您可能希望从存储库进行批量加载或删除整个子图或以非常特定的方式进行交互 使用 Neo4j Java-Driver。这也是可能的。以下示例演示如何:spring-doc.cadn.net.cn

使用普通驱动程序的片段
interface LowlevelInteractions {

    int deleteGraph();
}

class LowlevelInteractionsImpl implements LowlevelInteractions {

    private final Driver driver; (1)

    LowlevelInteractionsImpl(Driver driver) {
        this.driver = driver;
    }

    @Override
    public int deleteGraph() {

        try (Session session = driver.session()) {
            SummaryCounters counters = session
                    .executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()) (2)
                    .counters();
            return counters.nodesDeleted() + counters.relationshipsDeleted();
        }
    }
}
1 直接与Drivers合作。与所有示例一样:没有必要@Autowired魔法。所有碎片 实际上可以自行测试。
2 用例是编造的。在这里,我们使用驱动程序管理的事务删除整个图并返回 已删除的节点和关系

这种交互当然不会在 Spring 事务中运行,因为驱动程序不知道 Spring。spring-doc.cadn.net.cn

综上所述,此测试成功:spring-doc.cadn.net.cn

测试组合的存储库
@Test
void customRepositoryFragmentsShouldWork(
        @Autowired PersonRepository people,
        @Autowired MovieRepository movies
) {

    PersonEntity meg = people.findById("Meg Ryan").get();
    PersonEntity kevin = people.findById("Kevin Bacon").get();

    List<MovieEntity> moviesBetweenMegAndKevin = movies.
            findMoviesAlongShortestPath(meg, kevin);
    assertThat(moviesBetweenMegAndKevin).isNotEmpty();

    Collection<NonDomainResults.Result> relatedPeople = movies
            .findRelationsToMovie(moviesBetweenMegAndKevin.get(0));
    assertThat(relatedPeople).isNotEmpty();

    assertThat(movies.deleteGraph()).isGreaterThan(0);
    assertThat(movies.findAll()).isEmpty();
    assertThat(people.findAll()).isEmpty();
}

最后一句话:Spring Data Neo4j 会自动选择所有三个接口和实现。 无需进一步配置。 此外,可以只使用一个额外的片段(定义所有三种方法的接口)创建相同的整体存储库 和一个实现。实现将注入所有三个抽象(模板、客户端和驱动程序)。spring-doc.cadn.net.cn

当然,所有这些也适用于响应式存储库。 他们将与ReactiveNeo4jTemplateReactiveNeo4jClient以及驱动程序提供的响应式会话。spring-doc.cadn.net.cn

如果所有存储库都有重复方法,则可以交换默认存储库实现。spring-doc.cadn.net.cn

如何使用自定义 Spring Data Neo4j 基本存储库?

与共享的 Spring Data Commons 文档在自定义基本存储库中为 Spring Data JPA 显示的方式基本相同。 只是在我们的例子中,您将从spring-doc.cadn.net.cn

自定义基本存储库
public class MyRepositoryImpl<T, ID> extends SimpleNeo4jRepository<T, ID> {

    MyRepositoryImpl(
            Neo4jOperations neo4jOperations,
            Neo4jEntityInformation<T, ID> entityInformation
    ) {
        super(neo4jOperations, entityInformation); (1)
    }

    @Override
    public List<T> findAll() {
        throw new UnsupportedOperationException("This implementation does not support `findAll`");
    }
}
1 基类需要此签名。将Neo4jOperations(实际规格Neo4jTemplate) 和实体信息,并在需要时将它们存储在属性上。

在此示例中,我们禁止使用findAll方法。 您可以添加采用提取深度的方法,并根据该深度运行自定义查询。 DomainResults 片段中显示了一种执行此作的方法。spring-doc.cadn.net.cn

要为所有声明的存储库启用此基本存储库,请使用以下功能启用 Neo4j 存储库:@EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class).spring-doc.cadn.net.cn

如何审核实体?

支持所有 Spring Data 注释。 那些是spring-doc.cadn.net.cn

审计为您提供了如何在 Spring Data Commons 的更大上下文中使用审计的总体视图。 以下列表显示了 Spring Data Neo4j 提供的每个配置选项:spring-doc.cadn.net.cn

启用和配置 Neo4j 审计
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.domain.AuditorAware;

@Configuration
@EnableNeo4jAuditing(
        modifyOnCreate = false, (1)
        auditorAwareRef = "auditorProvider", (2)
        dateTimeProviderRef = "fixedDateTimeProvider" (3)
)
class AuditingConfig {

    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.of("A user");
    }

    @Bean
    public DateTimeProvider fixedDateTimeProvider() {
        return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE);
    }
}
1 如果您希望在创建过程中也写入修改数据,请设置为 true
2 使用此属性指定提供审计器的 Bean 的名称(即用户名)
3 使用此属性可以指定提供当前日期的 Bean 的名称。在这种情况下 使用固定日期,因为上述配置是我们测试的一部分

响应式版本基本相同,除了审计员感知 bean 的类型为ReactiveAuditorAware, 因此,审计员的检索是反应流的一部分。spring-doc.cadn.net.cn

除了这些审计机制之外,您还可以添加尽可能多的 bean,实现BeforeBindCallback<T>ReactiveBeforeBindCallback<T>到上下文。这些 bean 将被 Spring Data Neo4j 拾取并按顺序调用(如果它们实现Ordered或 用@Order) 在实体持久化之前。spring-doc.cadn.net.cn

他们可以修改实体或返回一个全新的实体。 以下示例向上下文添加一个回调,该回调在实体持久化之前更改一个属性:spring-doc.cadn.net.cn

保存前修改实体
import java.util.UUID;
import java.util.stream.StreamSupport;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.mapping.callback.AfterConvertCallback;
import org.springframework.data.neo4j.core.mapping.callback.BeforeBindCallback;

@Configuration
class CallbacksConfig {

    @Bean
    BeforeBindCallback<ThingWithAssignedId> nameChanger() {
        return entity -> {
            ThingWithAssignedId updatedThing = new ThingWithAssignedId(
                    entity.getTheId(), entity.getName() + " (Edited)");
            return updatedThing;
        };
    }

    @Bean
    AfterConvertCallback<ThingWithAssignedId> randomValueAssigner() {
        return (entity, definition, source) -> {
            entity.setRandomValue(UUID.randomUUID().toString());
            return entity;
        };
    }
}

无需额外配置。spring-doc.cadn.net.cn

如何使用“通过示例查找”?

“通过示例查找”是 SDN 中的一项新功能。 实例化实体或使用现有实体。 使用此实例,您可以创建一个org.springframework.data.domain.Example. 如果您的存储库扩展org.springframework.data.neo4j.repository.Neo4jRepositoryorg.springframework.data.neo4j.repository.ReactiveNeo4jRepository,您可以立即使用可用的findBy方法采用示例,如 findByExample 中所示。spring-doc.cadn.net.cn

findByExample 的实际应用
Example<MovieEntity> movieExample = Example.of(new MovieEntity("The Matrix", null));
Flux<MovieEntity> movies = this.movieRepository.findAll(movieExample);

movieExample = Example.of(
    new MovieEntity("Matrix", null),
    ExampleMatcher
        .matchingAny()
        .withMatcher(
            "title",
            ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
        )
);
movies = this.movieRepository.findAll(movieExample);

您还可以否定单个属性。这将添加适当的NOT作,从而将 an 变成 . 支持所有标量数据类型和所有字符串运算符:=<>spring-doc.cadn.net.cn

findByExample 使用否定值
Example<MovieEntity> movieExample = Example.of(
    new MovieEntity("Matrix", null),
    ExampleMatcher
        .matchingAny()
        .withMatcher(
            "title",
            ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
        )
       .withTransformer("title", Neo4jPropertyValueTransformers.notMatching())
);
Flux<MovieEntity> allMoviesThatNotContainMatrix = this.movieRepository.findAll(movieExample);

我需要 Spring Boot 才能使用 Spring Data Neo4j 吗?

不,你没有。 虽然通过 Spring Boot 自动配置许多 Spring 方面可以消除大量手动作,并且是设置新 Spring 项目的推荐方法,但您不必使用它。spring-doc.cadn.net.cn

上述解决方案需要以下依赖项:spring-doc.cadn.net.cn

<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-neo4j</artifactId>
	<version>7.4.9-SNAPSHOT</version>
</dependency>

Gradle 设置的坐标是相同的。spring-doc.cadn.net.cn

要选择不同的数据库(静态或动态),您可以添加类型为DatabaseSelectionProviderNeo4j 4 中所述,支持多个数据库 - 如何使用它们? 对于响应式场景,我们提供ReactiveDatabaseSelectionProvider.spring-doc.cadn.net.cn

在没有 Spring Boot 的 Spring 上下文中使用 Spring Data Neo4j

我们提供了两个抽象配置类来支持您引入必要的 bean:org.springframework.data.neo4j.config.AbstractNeo4jConfig用于命令式数据库访问和org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig对于响应式版本。 它们旨在与@EnableNeo4jRepositories@EnableReactiveNeo4jRepositories分别。 有关示例用法,请参阅启用 Spring Data Neo4j 基础结构以进行命令式数据库访问启用 Spring Data Neo4j 基础结构以进行响应式数据库访问。 这两个类都要求您覆盖driver()您应该在其中创建驱动程序。spring-doc.cadn.net.cn

要获取 Neo4j 客户端的命令式版本、模板和命令式存储库的支持,请使用类似于以下内容的内容:spring-doc.cadn.net.cn

启用 Spring Data Neo4j 基础设施以实现命令式数据库访问
import org.neo4j.driver.Driver;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.transaction.annotation.EnableTransactionManagement;

import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;

@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractNeo4jConfig {

    @Override @Bean
    public Driver driver() { (1)
        return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
    }

    @Override
    protected Collection<String> getMappingBasePackages() {
        return Collections.singletonList(Person.class.getPackage().getName());
    }

    @Override @Bean (2)
    protected DatabaseSelectionProvider databaseSelectionProvider() {

        return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
    }
}
1 驱动程序 bean 是必需的。
2 这会静态选择一个名为yourDatabase和是可选的

以下列表提供了响应式 Neo4j 客户端和模板,启用响应式事务管理并发现 Neo4j 相关存储库:spring-doc.cadn.net.cn

启用 Spring Data Neo4j 基础设施以进行响应式数据库访问
import org.neo4j.driver.Driver;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig;
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableReactiveNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractReactiveNeo4jConfig {

    @Bean
    @Override
    public Driver driver() {
        return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
    }

    @Override
    protected Collection<String> getMappingBasePackages() {
        return Collections.singletonList(Person.class.getPackage().getName());
    }
}

在 CDI 2.0 环境中使用 Spring Data Neo4j

为了您的方便,我们提供了 CDI 扩展Neo4jCdiExtension. 在兼容的 CDI 2.0 容器中运行时,它将通过 Java 的服务加载器 SPI 自动注册和加载。spring-doc.cadn.net.cn

您唯一需要引入应用程序的是生成 Neo4j Java 驱动程序的带注释的类型:spring-doc.cadn.net.cn

Neo4j Java 驱动程序的 CDI 生产者
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;

import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;

public class Neo4jConfig {

    @Produces @ApplicationScoped
    public Driver driver() { (1)
        return GraphDatabase
            .driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
    }

    public void close(@Disposes Driver driver) {
        driver.close();
    }

    @Produces @Singleton
    public DatabaseSelectionProvider getDatabaseSelectionProvider() { (2)
        return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
    }
}
1 启用 Spring Data Neo4j 基础设施以进行命令式数据库访问中的普通 Spring 相同,但使用相应的 CDI 基础设施进行注释。
2 这是可选的。但是,如果运行自定义数据库选择提供程序,则不得限定此 Bean。

如果您在 SE 容器中运行 - 例如 Weld 提供的容器,您可以像这样启用扩展:spring-doc.cadn.net.cn

在 SE 容器中启用 Neo4j CDI 扩展
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;

import org.springframework.data.neo4j.config.Neo4jCdiExtension;

public class SomeClass {
    void someMethod() {
        try (SeContainer container = SeContainerInitializer.newInstance()
                .disableDiscovery()
                .addExtensions(Neo4jCdiExtension.class)
                .addBeanClasses(YourDriverFactory.class)
                .addPackages(Package.getPackage("your.domain.package"))
            .initialize()
        ) {
            SomeRepository someRepository = container.select(SomeRepository.class).get();
        }
    }
}