常见问题解答

Neo4j-OGM 是一个对象图映射库,主要用于 Spring Data Neo4j 的早期版本,作为其后端来完成将节点和关系映射到领域对象的繁重工作。当前的 SDN **不需要** 并且**不支持** Neo4j-OGM。SDN 独占使用 Spring Data 的映射上下文来扫描类和构建元模型。

虽然这将 SDN 绑定到 Spring 生态系统,但它具有几个优点,其中包括更小的 CPU 和内存使用量,尤其重要的是 Spring 映射上下文的所有功能。

为什么我应该选择 SDN 而不是 SDN+OGM?

SDN 有一些 SDN+OGM 不具备的功能,特别是:

  • 完全支持 Spring 的响应式故事,包括响应式事务

  • 完全支持 按示例查询

  • 完全支持完全不可变的实体

  • 支持所有派生查找方法的修饰符和变体,包括空间查询

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

不支持。

SDN 是否支持嵌入式 Neo4j?

嵌入式 Neo4j 有多个方面。

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

不支持。

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

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

但是,SDN 可以很好地与 Neo4j 的测试工具配合使用,该测试工具专门设计为真实数据库的直接替代品。对 Neo4j 3.5、4.x 和 5.x 测试工具的支持是通过 驱动程序的 Spring Boot 启动器 实现的。请查看相应的模块 `org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure`。

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

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

因此,您可以将任何 4.x Neo4j Java 驱动程序与任何 SDN 6.x 版本一起使用,并将任何 5.x Neo4j 驱动程序与任何 SDN 7.x 版本一起使用。

使用 Spring Boot

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

从 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,我们建议使用 `` 部分,如下所示:

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

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

您可以静态配置数据库名称或运行您自己的数据库名称提供程序。请记住,SDN 不会为您创建数据库。您可以借助 迁移工具 或当然是一个简单的预先编写的脚本来完成此操作。

静态配置

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

spring.data.neo4j.database = yourDatabase

有了此配置,所有由 SDN 仓库实例(响应式和命令式)以及 `ReactiveNeo4jTemplate` 和 `Neo4jTemplate` 生成的查询都将在数据库 `yourDatabase` 上执行。

动态配置

提供一个类型为 `Neo4jDatabaseNameProvider` 或 `ReactiveDatabaseSelectionProvider` 的 Bean,具体取决于您的 Spring 应用程序的类型。

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

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 bean,并为每个实例提供对整体健康的贡献。但是,Neo4j 驱动程序连接到服务器,而不是服务器内的特定数据库。Spring Boot 能够在没有 Spring Data Neo4j 的情况下配置驱动程序,并且由于要使用的数据库信息与 Spring Data Neo4j 绑定,因此内置健康指标无法访问此信息。

在许多部署场景中,这很可能不是问题。但是,如果配置的数据库用户至少没有对默认数据库的访问权限,则健康检查将失败。

这可以通过了解数据库选择的自定义 Neo4j 健康贡献者来缓解。

命令式变体

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 用于检查连接是否健康的相同查询。使用以下配置来应用它

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 这可以防止各个贡献者直接显示在健康端点中

响应式变体

响应式变体基本上相同,使用响应式类型和相应的响应式基础结构类

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 也会包装现有的响应式指标以与非响应式执行器端点一起使用。

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+ 支持模拟不同的用户 - 我该如何使用它们?

用户模拟在大规模多租户设置中尤其有用,在一个物理连接(或技术)用户可以模拟许多租户的情况下。根据您的设置,这将大大减少所需的物理驱动程序实例数量。

此功能需要服务器端安装 Neo4j Enterprise 4.4+ 和客户端安装 4.4+ 驱动程序 (org.neo4j.driver:neo4j-java-driver:4.4.0 或更高版本)。

对于命令式和响应式版本,您都需要提供一个UserSelectionProviderReactiveUserSelectionProvider。相同的实例需要传递给Neo4ClientNeo4jTransactionManager 以及它们的响应式变体。

无 Boot 命令式响应式 配置中,您只需要提供该类型的一个 bean

用户选择提供程序 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 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 集群实例。

我是否需要特定的配置才能使事务与 Neo4j Causal Cluster 无缝协作?

不需要。SDN 在内部使用 Neo4j Causal Cluster 书签,无需您进行任何配置。在同一线程或同一响应式流中相互跟随的事务将能够读取您预期中先前更改的值。

对于 Neo4j 集群,使用只读事务是否重要?

是的,很重要。Neo4j 集群架构是一种因果集群架构,它区分主服务器和从服务器。主服务器要么是单个实例,要么是核心实例。两者都可以响应读写操作。写操作从核心实例传播到集群中的只读副本或更一般地说,跟随者。这些跟随者是从服务器。从服务器不响应写操作。

在标准部署场景中,您将在集群中拥有某些核心实例和许多只读副本。因此,将操作或查询标记为只读以扩展集群非常重要,这样领导者就不会不堪重负,并且查询尽可能多地传播到只读副本。

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

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

下面描述了一些选项

使整个存储库变为只读
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 在这里,对多个存储库的几次调用被包装在一个只读事务中。
在私有服务方法内和/或使用 Neo4j 客户端使用 Spring 的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 实例。当然,这也可以是一个全局 bean。
2 使用事务模板的第一个原因:声明性事务不适用于包私有或私有方法,也不适用于内部方法调用(想象一下此服务中的另一个方法调用internalOperation),因为它们的本质是使用方面和代理实现的。
3 Neo4jClient 是 SDN 提供的固定实用程序。它不能添加注解,但它与 Spring 集成。因此,它为您提供了使用纯驱动程序时所能执行的所有操作,而无需自动映射和事务。它也遵守声明性事务。

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

书签管理 中简要提到的,无需配置任何与书签相关的内容。但是,检索 SDN 事务系统从数据库接收的最新书签可能很有用。您可以添加一个类似BookmarkCapture@Bean 来执行此操作

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

要为事务系统播种,需要一个自定义事务管理器,如下所示

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 书签管理器,它有效地禁用了书签管理。

自行承担使用此书签管理器的风险,它将有效地通过丢弃所有书签并永不提供任何书签来禁用任何书签管理。在集群中,您将面临出现陈旧读取的高风险。在单个实例中,这很可能不会有任何区别。

+ 在集群中,只有当您可以容忍陈旧读取并且没有覆盖旧数据的危险时,这才是明智的方法。

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

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 对,但我们建议仅在您已为特定数据库选择需求配置它们时才这样做。

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

不需要。您可以随意使用以下等效的 Spring Data 注解

SDN 特定注解 Spring Data 通用注解 用途 区别

org.springframework.data.neo4j.core.schema.Id

org.springframework.data.annotation.Id

将带注解的属性标记为唯一 ID。

特定注解没有附加功能。

org.springframework.data.neo4j.core.schema.Node

org.springframework.data.annotation.Persistent

将类标记为持久性实体。

@Node 允许自定义标签

如何使用分配的 ID?

只需使用@Id 而无需@GeneratedValue,并通过构造函数参数、setter 或wither 填充您的 id 属性。有关查找良好 ID 的一些一般性说明,请参阅此 博文

如何使用外部生成的 ID?

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

ThingWithGeneratedId.java
@Node
public class ThingWithGeneratedId {

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

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

ThingWithIdGeneratedByBean.java
@Node
public class ThingWithIdGeneratedByBean {

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

这样,idGeneratingBean 指的是 Spring 上下文中的一個 bean。这可能对序列生成很有用。

对于 id,非 final 字段不需要 setter。

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

不需要。查看 SDN 构建块 并找到Neo4jTemplateReactiveNeo4jTemplate

这些模板知道您的域,并提供所有必要的用于检索、写入和计数实体的基本 CRUD 方法。

这是我们使用命令式模板的规范电影示例

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

}

这是响应式版本,为简洁起见省略了设置

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 中的@DataNeo4jTest

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

虽然您无需在返回Page<T>Slice<T> 的派生查找器方法上提供任何其他内容(除了Pageable 作为参数之外),但您必须准备您的自定义查询来处理分页。 页面和切片 为您概述了所需内容。

页面和切片
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>。Slice不知道总页数,因此自定义查询不需要专门的计数查询。SDN会通知您它估计了下一个Slice。Cypher模板必须同时包含$skip$limit Cypher参数。如果省略它们,SDN将发出警告。结果可能与您的预期不符。此外,Pageable应该未排序,并且您应该提供稳定的排序顺序。我们不会使用来自Pageable的排序信息。
3 此方法返回一个Page。Page知道确切的总页数。因此,您必须指定一个额外的计数查询。第二个方法中的所有其他限制都适用。

我可以映射命名路径吗?

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

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

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

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

它看起来像这样

image$bacon distance

我们找到了3个标记为Vertex的节点和2个标记为Movie的节点。两者都可以用自定义查询映射。假设VertexMovie以及处理关系的Actor都有一个节点实体

“标准”电影图域模型
@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”距离中所示的查询针对Vertex类型的域类时,例如

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之类的关系类型,并且这些关系类型也存在于域中,则将根据路径相应地填充这些关系类型。

使用基于路径查询水化的节点保存数据时,务必小心。如果并非所有关系都已水化,则数据将丢失。

反过来也一样。可以使用相同的查询与Movie实体一起使用。然后它只填充电影。以下清单显示了如何执行此操作以及如何使用路径上找不到的附加数据丰富查询。这些数据用于正确填充缺失的关系(在这种情况下,所有演员)

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

查询返回路径以及收集的所有关系和相关节点,以便完全水化电影实体。

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

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

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

不,@Query **不是**运行自定义查询的唯一方法。当您的自定义查询完全填充您的域时,此注解非常方便。请记住,SDN假设您的映射域模型是真实的。这意味着如果您通过@Query使用自定义查询,该查询只部分填充模型,则您有危险使用相同的对象来写回数据,这最终将擦除或覆盖您在查询中未考虑的数据。

因此,在结果形状像您的域模型或您确定不使用部分映射模型进行写入命令的所有情况下,请使用存储库和声明性方法与@Query

有哪些替代方法?

  • 投影可能已经足以塑造您对图的**视图**:它们可以用来以显式的方式定义获取属性和相关实体的深度:通过建模。

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

  • 通过CypherdslStatementExecutorReactiveCypherdslStatementExecutor使用Cypher-DSL。Cypher-DSL 预先设计用于创建动态查询。最终,这就是SDN在后台使用的。相应的mixins既适用于存储库本身的域类型,也适用于投影(添加条件的mixins不具备此功能)。

如果您认为可以使用部分动态查询或完整的动态查询以及投影来解决您的问题,请立即返回到关于Spring Data Neo4j Mixins的章节。

否则,请阅读以下两方面内容:自定义存储库片段以及我们在SDN中提供的抽象级别

为什么现在要讨论自定义存储库片段?

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

  • 您的自定义查询返回一个形状结果图,该图不太适合您的域模型,因此自定义查询也应伴随自定义映射。

  • 您需要与驱动程序交互,例如对于不应通过对象映射进行的大批量加载。

假设以下基本聚合了一个基本存储库和 3 个片段的存储库 *声明*

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

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

该存储库包含电影,如入门部分所示。

存储库扩展的附加接口(DomainResultsNonDomainResultsLowlevelInteractions)是解决上述所有问题的片段。

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

片段DomainResults声明了一个附加方法findMoviesAlongShortestPath

DomainResults 片段
interface DomainResults {

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

此方法用@Transactional(readOnly = true)注解,表明读取器可以回答它。它不能由SDN派生,但需要自定义查询。此自定义查询由该接口的一个实现提供。该实现具有相同的名称,后缀为Impl

使用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 用于构建复杂的语句(与路径映射中显示的语句几乎相同)。该语句可以直接传递给模板。

模板也支持基于字符串的查询的重载,因此您也可以将查询编写为字符串。这里重要的收获是

  • 模板“知道”您的域对象并相应地对其进行映射

  • @Query不是定义自定义查询的唯一选项

  • 它们可以通过多种方式生成

  • @Transactional注解得到尊重

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

自定义查询通常表示自定义结果。所有这些结果都应该映射为@Node吗?当然不是!很多时候这些对象代表读取命令,并非旨在用作写入命令。SDN 也可能无法或不想映射 Cypher 可能实现的所有内容。但是,它确实提供了一些挂钩来运行您自己的映射:在Neo4jClient上。使用SDN Neo4jClient而不是驱动程序的好处是

  • Neo4jClient与Spring的事务管理集成

  • 它具有用于绑定参数的流畅 API

  • 它具有一个流畅的 API,公开记录和 Neo4j 类型系统,以便您可以访问结果中的所有内容来执行映射

声明片段与之前完全相同

声明非域类型结果的片段
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注解

如果没有该片段的实现,启动将失败,因此这里提供了实现

使用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事务的上下文中运行,在这种情况下是只读事务。

低级交互

有时您可能希望从存储库进行批量加载或删除整个子图,或者以非常特殊的方式与Neo4j Java驱动程序交互。这也是可能的。以下示例显示了如何操作

使用纯驱动程序的片段
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。

将所有内容放在一起,此测试成功

测试组合存储库
@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会自动拾取所有三个接口和实现。无需进一步配置。此外,可以使用一个附加片段(定义所有三个方法的接口)和一个实现来创建相同的整体存储库。然后,实现将注入所有三个抽象(模板、客户端和驱动程序)。

所有这些当然也适用于反应式存储库。它们将使用ReactiveNeo4jTemplateReactiveNeo4jClient以及驱动程序提供的反应式会话。

如果您对所有存储库都有重复出现的方法,则可以替换默认的存储库实现。

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

基本上与 Spring Data Commons 文档中针对 Spring Data JPA 所示的方法相同,参见自定义基础仓库。只是在我们的例子中,您需要继承自

自定义基础仓库
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 片段所示。

要为所有已声明的仓库启用此基础仓库,请使用以下方式启用 Neo4j 仓库:@EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class)

如何审计实体?

所有 Spring Data 注解都受支持。它们是

  • org.springframework.data.annotation.CreatedBy

  • org.springframework.data.annotation.CreatedDate

  • org.springframework.data.annotation.LastModifiedBy

  • org.springframework.data.annotation.LastModifiedDate

审计 提供了如何在 Spring Data Commons 更大的环境中使用审计的总体视图。以下清单列出了 Spring Data Neo4j 提供的每个配置选项

启用和配置 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`,以便审计者的检索成为响应式流程的一部分。

除了这些审计机制外,您还可以向上下文中添加任意数量实现 `BeforeBindCallback<T>` 或 `ReactiveBeforeBindCallback<T>` 的 Bean。这些 Bean 将由 Spring Data Neo4j 拾取并按顺序调用(如果它们实现了 `Ordered` 或用 `@Order` 进行了注解),这发生在实体持久化之前。

它们可以修改实体或返回一个全新的实体。以下示例向上下文添加一个回调,该回调在实体持久化之前更改一个属性

保存前修改实体
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;
        };
    }
}

无需额外配置。

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

“按示例查找”是 SDN 的一项新功能。您可以实例化一个实体或使用现有实体。使用此实例,您可以创建一个 `org.springframework.data.domain.Example`。如果您的仓库继承自 `org.springframework.data.neo4j.repository.Neo4jRepository` 或 `org.springframework.data.neo4j.repository.ReactiveNeo4jRepository`,则您可以立即使用可用的 `findBy` 方法来传入示例,如findByExample所示。

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` 操作,从而将 `=` 转换为 `<>`。所有标量数据类型和所有字符串运算符都受支持

带有否定值的 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 Data Neo4j 是否需要 Spring Boot?

不需要。虽然 Spring Boot 通过 Spring 的自动配置消除了许多手动操作,并且是设置新 Spring 项目的推荐方法,但您无需使用它。

上述解决方案需要以下依赖项

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

Gradle 设置的坐标相同。

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

在没有 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()`,您应该在其中创建驱动程序。

要获取Neo4j 客户端 的命令式版本、模板和命令式仓库的支持,请使用此处所示的类似内容

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

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

在 CDI 2.0 环境中使用 Spring Data Neo4j

为方便起见,我们提供了一个带有 `Neo4jCdiExtension` 的 CDI 扩展。在兼容的 CDI 2.0 容器中运行时,它将通过Java 的服务加载器 SPI自动注册和加载。

您只需要在应用程序中引入一个带注解的类型来生成 Neo4j Java 驱动程序

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://127.0.0.1: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 提供的容器),您可以像这样启用扩展

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