最新的稳定版本请使用 Spring Data Neo4j 7.3.1Spring中文文档

最新的稳定版本请使用 Spring Data Neo4j 7.3.1Spring中文文档

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

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

为什么我应该使用 SDN 而不是 SDN+OGM

SDN 具有 SDN+OGM 中不存在的几个功能,特别是Spring中文文档

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

SDN是否支持嵌入式Neo4j?

嵌入式 Neo4j 具有多个方面:Spring中文文档

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

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

不。 嵌入式数据库通常由 的实例表示,并且没有开箱即用的 Bolt 连接器。org.neo4j.graphdb.GraphDatabaseServiceSpring中文文档

然而,SDN 可以与 Neo4j 的测试工具一起工作,测试工具专门用于替代真实数据库。 对 Neo4j 3.5、4.x 和 5.x 测试工具的支持是通过驱动程序的 Spring Boot 启动器实现的。 看看对应的模块。org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigureSpring中文文档

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

SDN 依赖于 Neo4j Java 驱动程序。 每个 SDN 版本都使用与最新的 Neo4j 兼容的 Neo4j Java 驱动程序版本,该版本在其发布时可用 释放。 虽然 Neo4j Java 驱动程序的补丁版本通常是直接替换的,但 SDN 确保即使是次要版本 是可互换的,因为它会检查是否存在方法或必要时的接口更改。Spring中文文档

因此,您可以在任何 SDN 6.x 版本中使用任何 4.x Neo4j Java 驱动程序, 以及任何 SDN 7.x 版本的任何 5.x Neo4j 驱动程序。Spring中文文档

使用 Spring Boot

如今,Spring Boot 部署是最有可能部署基于 Spring Data 的应用程序。请使用 Spring Boots 依赖项管理来更改驱动程序版本,如下所示:Spring中文文档

从 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中文文档

从 Maven 更改没有 Spring Boot 的驱动程序版本 (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中文文档

静态配置

像这样配置要在 Spring Boot 配置中使用的数据库名称(当然,相同的属性适用于 YML 或基于环境的配置,并应用了 Spring Boot 的约定):Spring中文文档

spring.data.neo4j.database = yourDatabase

配置到位后,SDN 存储库的所有实例(响应式和命令式)以及分别生成的所有查询都将针对数据库执行。ReactiveNeo4jTemplateNeo4jTemplateyourDatabaseSpring中文文档

动态配置

提供具有类型或取决于 Spring 应用程序类型的 Bean。Neo4jDatabaseNameProviderReactiveDatabaseSelectionProviderSpring中文文档

例如,该 Bean 可以使用 Spring 的安全上下文来检索租户。 下面是一个受 Spring Security 保护的命令式应用程序的工作示例:Spring中文文档

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 健康指标。这两种变体都能够检测应用程序上下文中的多个 Bean,并提供 对每个实例的整体运行状况的贡献。 但是,Neo4j 驱动程序确实连接到服务器,而不是该服务器内的特定数据库。 Spring Boot能够在没有Spring Data Neo4j的情况下配置驱动程序,并作为要使用的数据库的信息 绑定到 Spring Data Neo4j,此信息不可用于内置运行状况指示器。org.neo4j.driver.DriverSpring中文文档

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

这可以通过了解数据库选择的自定义 Neo4j 运行状况参与者来缓解。Spring中文文档

命令式变体

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

这将使用可用的数据库选择来运行与启动运行相同的查询,以检查连接是否正常。 使用以下配置来应用它:Spring中文文档

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中文文档

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中文文档

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);
    }
}
请注意,不要将从一个数据库检索到的实体与另一个数据库混为一谈。 每个新事务都需要数据库名称,因此在两次调用之间更改数据库名称时,最终的实体数量可能会少于预期或多于预期。 或者更糟糕的是,您可能不可避免地将错误的实体存储在错误的数据库中。
1 如果有多个驱动程序和数据库选择提供程序,则需要为每个组合创建一个指示器
2 这确保了所有这些指标都归入 Neo4j 下,取代了默认的 Neo4j 健康指标
3 这样可以防止各个参与者直接显示在运行状况终结点中

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

用户模拟在大型多租户设置中特别有趣,其中一个人物理连接(或技术) 用户可以模拟多个租户。根据你的设置,这将大大减少所需的物理驱动程序实例的数量。Spring中文文档

该功能需要服务器端的 Neo4j Enterprise 4.4+ 和客户端的 4.4+ 驱动程序(或更高版本)。org.neo4j.driver:neo4j-java-driver:4.4.0Spring中文文档

对于命令式和反应式版本,您需要分别提供 . 需要将相同的实例传递给它们的反应变体。UserSelectionProviderReactiveUserSelectionProviderNeo4ClientNeo4jTransactionManagerSpring中文文档

无引导命令式和反应式配置中,您只需要提供 有问题的类型:Spring中文文档

用户选择提供程序 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中文文档

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中文文档

我是否需要特定的配置才能使事务与 Neo4j 因果集群无缝协作?

不,你没有。 SDN在内部使用Neo4j Causal Cluster书签,无需您进行任何配置。 同一线程或同一反应式流中的事务将能够读取其先前更改的值,如您所料。Spring中文文档

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

是的,它是。 Neo4j 集群架构是一种因果集群架构,它区分了主服务器和辅助服务器。 主服务器可以是单实例,也可以是核心实例。它们都可以回答读取和写入操作。 写入操作从核心实例传播到集群内的只读副本或更一般的从属实例。 这些追随者是辅助服务器。 辅助服务器不应答写入操作。Spring中文文档

在标准部署方案中,群集中将有一些核心实例和许多只读副本。 因此,请务必将操作或查询标记为只读,以便以领导者的方式扩展群集 永远不会不堪重负,并且会尽可能多地传播查询以读取副本。Spring中文文档

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

默认情况下,所有 、 和预定义的存在方法都标记为只读。findByIdfindAllByIdfindAll

下面介绍了一些选项:Spring中文文档

使整个存储库只读
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 为什么这不是默认的只读?虽然它适用于上面的派生查找器(我们实际上知道它是只读的), 我们经常看到用户添加自定义并通过构造实现它的情况, 这当然是写入操作。@QueryMERGE
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 在这里,对多个存储库的多次调用被包装在一个单个只读事务中。
在专用服务方法和/或Neo4j客户端中使用弹簧TransactionTemplate
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
2 使用交易模板的第一个原因:声明易不起作用 在包私有或私有方法中,也不在内部方法调用中(想象一下另一种方法 在此服务调用中),因为它们的性质是通过 Aspects 实现的 和代理。internalOperation
3 这是 SDN 提供的固定实用程序。它不能被注释,但它与 Spring 集成。 因此,它为您提供了使用纯驱动程序执行的所有操作,而无需自动映射,并且使用 交易。它还服从声明性事务。Neo4jClient

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

书签管理中简要提到的,无需对书签进行任何配置。 但是,检索 SDN 事务系统从数据库接收的最新书签可能很有用。 您可以添加一个喜欢来执行此操作:@BeanBookmarkCaptureSpring中文文档

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中文文档

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中文文档

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

+ 在集群中,这只能是一种明智的方法,只要您可以容忍过时的读取并且没有危险 覆盖旧数据。Spring中文文档

以下配置创建书签管理器的“noop”变体,该变体将从相关类中选取。Spring中文文档

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/ReactiveNeo4jClientSpring中文文档

默认情况下,所有 、 和预定义的存在方法都标记为只读。findByIdfindAllByIdfindAll
1 为什么这不是默认的只读?虽然它适用于上面的派生查找器(我们实际上知道它是只读的), 我们经常看到用户添加自定义并通过构造实现它的情况, 这当然是写入操作。@QueryMERGE
2 自定义过程可以做各种各样的事情,目前没有办法在这里为我们检查只读与写入。
1 在这里,对多个存储库的多次调用被包装在一个单个只读事务中。
1 创建具有所需特征的实例。 当然,这也可以是全球豆。TransactionTemplate
2 使用交易模板的第一个原因:声明易不起作用 在包私有或私有方法中,也不在内部方法调用中(想象一下另一种方法 在此服务调用中),因为它们的性质是通过 Aspects 实现的 和代理。internalOperation
3 这是 SDN 提供的固定实用程序。它不能被注释,但它与 Spring 集成。 因此,它为您提供了使用纯驱动程序执行的所有操作,而无需自动映射,并且使用 交易。它还服从声明性事务。Neo4jClient
1 让 Spring 注入那些
2 该供应商可以是任何包含您要带入系统的最新书签的东西
3 用它创建书签管理器
4 将其传递给自定义事务管理器
无需执行上述任何操作,除非您的应用程序需要访问或提供 此数据。如有疑问,也不要这样做。
使用此书签管理器的风险由您自己承担,它将通过删除所有书签来有效地禁用任何书签管理 书签,从不提供任何书签。在群集中,您将面临遇到过时读取的高风险。在单个 实例它很可能不会有任何区别。

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

不。 您可以自由使用以下等效的 Spring Data 注释:Spring中文文档

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

org.springframework.data.neo4j.core.schema.IdSpring中文文档

org.springframework.data.annotation.IdSpring中文文档

将带批注的属性标记为唯一 ID。Spring中文文档

特定注释没有其他功能。Spring中文文档

org.springframework.data.neo4j.core.schema.NodeSpring中文文档

org.springframework.data.annotation.PersistentSpring中文文档

将类标记为持久性实体。Spring中文文档

@Node允许自定义标签Spring中文文档

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

org.springframework.data.neo4j.core.schema.IdSpring中文文档

org.springframework.data.annotation.IdSpring中文文档

将带批注的属性标记为唯一 ID。Spring中文文档

特定注释没有其他功能。Spring中文文档

org.springframework.data.neo4j.core.schema.NodeSpring中文文档

org.springframework.data.annotation.PersistentSpring中文文档

将类标记为持久性实体。Spring中文文档

@Node允许自定义标签Spring中文文档

如何使用分配的 ID?

只需使用 without 并通过构造函数参数或 setter 或 wither 填充 id 属性即可。 请参阅这篇博文,了解有关查找良好 ID 的一些一般评论。@Id@GeneratedValueSpring中文文档

如何使用外部生成的 ID?

我们提供接口。 以您想要的任何方式实现它,并像这样配置您的实现:org.springframework.data.neo4j.core.schema.IdGeneratorSpring中文文档

ThingWithGeneratedId.java
@Node
public class ThingWithGeneratedId {

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

如果将类的名称传递给 ,则此类必须具有 no-args 默认构造函数。 但是,您也可以使用字符串:@GeneratedValueSpring中文文档

ThingWithIdGeneratedByBean.java
@Node
public class ThingWithIdGeneratedByBean {

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

有了它,指的是 Spring 上下文中的 bean。 这可能对序列生成有用。idGeneratingBeanSpring中文文档

在 id 的非最终字段上不需要 Setter。
在 id 的非最终字段上不需要 Setter。

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

不。 查看 SDN 构建块,并找到 或 .Neo4jTemplateReactiveNeo4jTemplateSpring中文文档

这些模板了解您的域,并提供检索、写入和计数实体所需的所有基本 CRUD 方法。Spring中文文档

这是我们使用命令式模板的规范电影示例:Spring中文文档

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中文文档

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

请注意,这两个示例都使用 Spring Boot。@DataNeo4jTestSpring中文文档

如何使用带有返回 或 的存储库方法的自定义查询?Page<T>Slice<T>

虽然您不必提供任何其他内容,但除了 a 作为派生查找器方法的参数 返回 A 或 A ,您必须准备自定义查询来处理可分页查询。“页面和分讯”为您提供了有关所需内容的概述。PageablePage<T>Slice<T>Spring中文文档

页面和分片
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 此方法用于定义自定义查询。它返回一个 . 切片不知道总页数,因此自定义查询 不需要专用的计数查询。SDN 将通知您,它估计了下一个切片。 Cypher 模板必须同时发现 Cypher 参数。 如果省略它们,SDN 将发出警告。可能不符合您的期望。 此外,应该是未排序的,您应该提供稳定的订单。 我们不会使用可分页的排序信息。@QuerySlice<Person>$skip$limitPageable
3 此方法返回一个页面。页面知道总页数的确切数。 因此,您必须指定其他计数查询。 第二种方法的所有其他限制都适用。
1 为您创建查询的派生查找器方法。 它为您处理。 您应该使用排序的可分页。Pageable
2 此方法用于定义自定义查询。它返回一个 . 切片不知道总页数,因此自定义查询 不需要专用的计数查询。SDN 将通知您,它估计了下一个切片。 Cypher 模板必须同时发现 Cypher 参数。 如果省略它们,SDN 将发出警告。可能不符合您的期望。 此外,应该是未排序的,您应该提供稳定的订单。 我们不会使用可分页的排序信息。@QuerySlice<Person>$skip$limitPageable
3 此方法返回一个页面。页面知道总页数的确切数。 因此,您必须指定其他计数查询。 第二种方法的所有其他限制都适用。

我可以映射命名路径吗?

在 Neo4j 中,一系列连接的节点和关系被称为“路径”。 Cypher 允许使用标识符命名路径,例如:Spring中文文档

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

或者像臭名昭著的电影图一样,它包括以下路径(在这种情况下,是两个演员之间的最短路径之一):Spring中文文档

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

它看起来像这样:Spring中文文档

图片$培根距离

我们发现 3 个节点被标记,2 个节点被标记。两者都可以使用自定义查询进行映射。 假设两者都有一个节点实体,并且负责处理关系:VertexMovieVertexMovieActorSpring中文文档

“标准”电影图域模型
@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;
}

使用如下类型的域类的“Bacon”距离中所示的查询时VertexSpring中文文档

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

它将从路径中检索所有人并绘制他们。 如果路径上存在类似的关系类型,则域中也存在此类关系类型,则这些 将从路径中相应地填充。REVIEWEDSpring中文文档

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

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

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中文文档

路径映射适用于单个路径以及多个路径记录(由函数返回)。allShortestPathSpring中文文档

命名路径可以有效地用于填充和返回不仅仅是根节点,请参阅 appendix/custom-queries.adoc#custom-query.paths
使用从基于路径的查询冻结的节点来保存数据时,请特别小心。 如果不是所有关系都已水合,则数据将丢失。
命名路径可以有效地用于填充和返回不仅仅是根节点,请参阅 appendix/custom-queries.adoc#custom-query.paths

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

否,不是运行自定义查询的唯一方法。 在自定义查询完全填满域的情况下,注释很舒服。 请记住,SDN 假定映射的域模型是真实的。 这意味着,如果您使用仅部分填充模型的自定义查询,则有可能使用相同的查询 对象写回数据,这最终将擦除或覆盖查询中未考虑的数据。@Query@QuerySpring中文文档

因此,请使用存储库和声明性方法,在所有情况下,结果的形状都像您的域一样 模型,或者您确定不会将部分映射的模型用于写入命令。@QuerySpring中文文档

有哪些替代方案?Spring中文文档

  • 投影可能已经足以塑造您在图形上的视图:它们可用于定义 以显式方式获取属性和相关实体的深度:通过对它们进行建模。Spring中文文档

  • 如果您的目标是仅使查询的条件动态化,请查看 QuerydslPredicateExecutor,尤其是我们自己的变体 .两种混合素都允许添加条件 我们为您创建的完整查询。因此,您将拥有完全填充域以及自定义条件。 当然,您的条件必须与我们生成的内容相适应。在此处查找根节点、相关节点等的名称。CypherdslConditionExecutorSpring中文文档

  • 通过 或 使用 Cypher-DSL。 Cypher-DSL 注定要创建动态查询。归根结底,这是SDN在引擎盖下使用的。相应的 mixins 既可以与存储库本身的域类型一起使用,也可以与投影一起使用(mixins 用于添加的东西 条件没有)。CypherdslStatementExecutorReactiveCypherdslStatementExecutorSpring中文文档

如果您认为可以使用部分动态查询或完全动态查询以及投影来解决您的问题, 现在请跳回有关Spring Data Neo4j Mixins的章节。Spring中文文档

否则,请仔细阅读两件事:自定义存储库碎片化我们在 SDN 中提供的抽象级别Spring中文文档

为什么现在谈论自定义存储库片段?Spring中文文档

  • 您可能会遇到更复杂的情况,即需要多个动态查询,但这些查询仍然属于 从概念上讲,在存储库中,而不是在服务层中Spring中文文档

  • 自定义查询返回的图形形状结果与域模型不太匹配 因此,自定义查询还应附带自定义映射Spring中文文档

  • 您需要与驱动程序进行交互,即对于不应通过对象映射的批量加载。Spring中文文档

假设以下存储库声明基本上聚合了一个基本存储库和 3 个片段:Spring中文文档

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

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

存储库从中扩展的附加接口 (, 和 ) 是解决上述所有问题的片段。DomainResultsNonDomainResultsLowlevelInteractionsSpring中文文档

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

该片段声明了一种附加方法:DomainResultsfindMoviesAlongShortestPathSpring中文文档

DomainResults 片段
interface DomainResults {

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

此方法带有注释,以指示读者可以回答它。 它不能由 SDN 派生,但需要自定义查询。 此自定义查询由该接口的一个实现提供。 该实现具有相同的名称,后缀为:@Transactional(readOnly = true)ImplSpring中文文档

使用 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;
import org.neo4j.cypherdsl.core.Functions;

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(Functions.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, Functions.collect(name("r")), Functions.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 由运行时通过 的构造函数注入。不需要 .Neo4jTemplateDomainResultsImpl@Autowired
2 Cypher-DSL 用于构建复杂的语句(与路径映射中所示的语句几乎相同)。 该语句可以直接传递到模板。

该模板还具有基于字符串的查询的重载,因此您也可以将查询写为 String。 这里的重要要点是:Spring中文文档

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

通常,自定义查询指示自定义结果。 所有这些结果都应该映射为?当然不是!很多时候,这些对象表示读取命令 并且不用作写入命令。 SDN不能或不想映射Cypher可能的一切,这也不是不可能。 但是,它确实提供了几个钩子来运行您自己的映射:在 . 在驱动程序上使用 SDN 的好处:@NodeNeo4jClientNeo4jClientSpring中文文档

声明片段与之前完全相同:Spring中文文档

声明非域类型结果的片段
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中文文档

使用 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 客户端只接受 Strings,但在呈现为 String 时仍可以使用 Cypher-DSL
3 将一个值绑定到命名参数。还有一个重载来绑定整个参数映射
4 这是您想要的结果类型
5 最后是该方法,为结果中的每个条目公开一个,如果需要,还可以公开驱动程序类型系统。 这是您挂接自定义映射的 APImappedByRecord

整个查询在 Spring 事务的上下文中运行,在本例中为只读事务。Spring中文文档

低级交互

有时,您可能希望从存储库中批量加载或删除整个子图或以非常特定的方式进行交互 使用 Neo4j Java-Driver。这也是可能的。以下示例演示如何操作:Spring中文文档

使用普通驱动程序的片段
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 直接与驱动程序合作。就像所有的例子一样:不需要魔法。所有片段 实际上可以单独测试。@Autowired
2 用例是虚构的。这里我们使用一个驱动程序管理的事务来删除整个图并返回 已删除的节点和关系

当然,这种交互不会在 Spring 事务中运行,因为驱动程序不知道 Spring。Spring中文文档

综上所述,此测试成功:Spring中文文档

测试组合存储库
@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中文文档

当然,所有这些也适用于响应式存储库。 它们将使用驱动程序提供的 and 和 reactive 会话。ReactiveNeo4jTemplateReactiveNeo4jClientSpring中文文档

如果所有存储库都有重复方法,则可以换掉默认存储库实现。Spring中文文档

1 由运行时通过 的构造函数注入。不需要 .Neo4jTemplateDomainResultsImpl@Autowired
2 Cypher-DSL 用于构建复杂的语句(与路径映射中所示的语句几乎相同)。 该语句可以直接传递到模板。
1 这是一个虚构的非域结果。实际查询结果可能看起来更复杂。
2 此片段添加的方法。同样,该方法用 Spring 的@Transactional
1 在这里,我们使用基础结构提供的 。Neo4jClient
2 客户端只接受 Strings,但在呈现为 String 时仍可以使用 Cypher-DSL
3 将一个值绑定到命名参数。还有一个重载来绑定整个参数映射
4 这是您想要的结果类型
5 最后是该方法,为结果中的每个条目公开一个,如果需要,还可以公开驱动程序类型系统。 这是您挂接自定义映射的 APImappedByRecord
1 直接与驱动程序合作。就像所有的例子一样:不需要魔法。所有片段 实际上可以单独测试。@Autowired
2 用例是虚构的。这里我们使用一个驱动程序管理的事务来删除整个图并返回 已删除的节点和关系

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

与共享的Spring Data Commons文档在自定义基本存储库中为Spring Data JPA显示的方式基本相同。 只有在我们的情况下,您会从Spring中文文档

自定义基础存储库
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 基类需要此签名。取(实际规格的) 和实体信息,并在需要时将它们存储在属性上。Neo4jOperationsNeo4jTemplate

在此示例中,我们禁止使用该方法。 您可以添加获取深度的方法,并基于该深度运行自定义查询。 DomainResults 片段中显示了执行此操作的一种方法。findAllSpring中文文档

要为所有声明的存储库启用此基本存储库,请使用以下命令启用 Neo4j 存储库: 。@EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class)Spring中文文档

1 基类需要此签名。取(实际规格的) 和实体信息,并在需要时将它们存储在属性上。Neo4jOperationsNeo4jTemplate

如何审计实体?

支持所有 Spring Data 注释。 这些是Spring中文文档

审计为您提供了如何在 Spring Data Commons 的更大上下文中使用审计的一般视图。 以下列表显示了 Spring Data Neo4j 提供的每个配置选项:Spring中文文档

启用和配置 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 是类型的, 因此,审计员的检索是反应流的一部分。ReactiveAuditorAwareSpring中文文档

除了这些审计机制之外,您还可以在上下文中添加任意数量的 bean、实现或。这些 bean 将被 Spring Data Neo4j 拾取并按顺序调用(以防它们实现或 在实体被持久化之前用 ) 进行注释。BeforeBindCallback<T>ReactiveBeforeBindCallback<T>Ordered@OrderSpring中文文档

他们可以修改实体或返回一个全新的实体。 以下示例向上下文添加一个回调,该回调在保留实体之前更改一个属性:Spring中文文档

保存前修改实体
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中文文档

1 如果还希望在创建过程中写入修改数据,则设置为 true
2 使用此属性指定提供审计员的 Bean 的名称(即用户名)
3 使用此属性可以指定提供当前日期的 Bean 的名称。在这种情况下 使用固定日期,因为上述配置是我们测试的一部分

如何使用“按示例查找”?

“按示例查找”是 SDN 中的一项新功能。 实例化实体或使用现有实体。 使用此实例,您可以创建一个 . 如果您的存储库扩展了 ,则可以立即使用示例中的可用方法,如 findByExample 中所示。org.springframework.data.domain.Exampleorg.springframework.data.neo4j.repository.Neo4jRepositoryorg.springframework.data.neo4j.repository.ReactiveNeo4jRepositoryfindBySpring中文文档

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=<>Spring中文文档

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中文文档

上述解决方案需要以下依赖项:Spring中文文档

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

Gradle 设置的坐标相同。Spring中文文档

要选择不同的数据库 - 静态或动态 - 您可以添加一个 Bean 类型,如 Neo4j 4 支持多个数据库 - 如何使用它们?中所述。 对于反应式场景,我们提供 .DatabaseSelectionProviderReactiveDatabaseSelectionProviderSpring中文文档

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

我们提供了两个抽象配置类来支持您引入必要的 bean:用于命令式数据库访问和响应式版本。 它们旨在分别与 和 一起使用。 有关示例用法,请参阅启用 Spring Data Neo4j 基础结构以进行命令式数据库访问启用 Spring Data Neo4j 基础结构以进行反应式数据库访问。 这两个类都要求您重写应在其中创建驱动程序的位置。org.springframework.data.neo4j.config.AbstractNeo4jConfigorg.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig@EnableNeo4jRepositories@EnableReactiveNeo4jRepositoriesdriver()Spring中文文档

要获取 Neo4j 客户端的命令式版本、模板和对命令式存储库的支持,请使用类似于此处所示的内容:Spring中文文档

启用 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 这将静态选择名为 and 的数据库,该数据库是可选的yourDatabase

以下列表提供了响应式 Neo4j 客户端和模板,启用响应式事务管理并发现与 Neo4j 相关的存储库:Spring中文文档

启用 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 扩展名的 CDI 扩展。 当在兼容的 CDI 2.0 容器中运行时,它将通过 Java 的服务加载器 SPI 自动注册和加载。Neo4jCdiExtensionSpring中文文档

你唯一需要带入应用程序的是生成 Neo4j Java 驱动程序的注释类型:Spring中文文档

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中文文档

在 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();
        }
    }
}
1 驱动程序 Bean 是必需的。
2 这将静态选择名为 and 的数据库,该数据库是可选的yourDatabase
1 启用 Spring Data Neo4j 基础架构中的普通 Spring 相同,用于命令式数据库访问,但使用相应的 CDI 基础结构进行注释。
2 这是可选的。但是,如果运行自定义数据库选择提供程序,则不得限定此 Bean。