常见问题解答

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

虽然这使得 SDN 与 Spring 生态系统紧密相连,但它有几个优点,其中包括更小的 CPU 和内存占用,特别是 Spring 映射上下文的所有特性。

为什么我应该优先使用 SDN 而非 SDN+OGM

SDN 拥有 SDN+OGM 中没有的几个特性,特别是

  • 全面支持 Spring 的响应式编程模型,包括响应式事务

  • 全面支持 按示例查询 (Query By Example)

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

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

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

不支持。

SDN 是否支持嵌入式 Neo4j?

嵌入式 Neo4j 有多个方面:

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

不支持。

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

不支持。嵌入式数据库通常由 org.neo4j.graphdb.GraphDatabaseService 的实例表示,默认没有 Bolt 连接器。

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

可以使用哪些 Neo4j Java Driver,以及如何使用?

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

因此,您可以使用任何 4.x 版本的 Neo4j Java Driver 与任何 6.x 版本的 SDN,以及任何 5.x 版本的 Neo4j Driver 与任何 7.x 版本的 SDN。

使用 Spring Boot

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

通过 Maven (pom.xml) 更改 Driver 版本
<properties>
  <neo4j-java-driver.version>5.4.0</neo4j-java-driver.version>
</properties>

或者

通过 Gradle (gradle.properties) 更改 Driver 版本
neo4j-java-driver.version = 5.4.0

不使用 Spring Boot

不使用 Spring Boot 时,您只需手动声明依赖。对于 Maven,我们建议使用 <dependencyManagement /> 部分,如下所示:

不使用 Spring Boot 时通过 Maven (pom.xml) 更改 Driver 版本
<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 Repository 实例(无论是响应式还是命令式)以及 ReactiveNeo4jTemplateNeo4jTemplate 生成的所有查询都将针对 yourDatabase 数据库执行。

动态配置

根据您的 Spring 应用程序类型,提供一个类型为 Neo4jDatabaseNameProviderReactiveDatabaseSelectionProvider 的 Bean。

该 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 Driver 连接的是服务器,而不是服务器内的特定数据库。Spring Boot 能够在不依赖 Spring Data Neo4j 的情况下配置 Driver,并且由于要使用的数据库信息与 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 如果您有多个 Driver 和数据库选择提供者,则需要为每种组合创建一个指标。
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 也会包装现有的响应式指标以用于非响应式 actuator 端点。

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

用户模拟在大型多租户设置中特别有趣,其中一个物理连接(或技术)用户可以模拟许多租户。根据您的设置,这将大大减少所需的物理 Driver 实例数量。

该特性要求服务器端使用 Neo4j Enterprise 4.4+,客户端使用 4.4+ Driver (org.neo4j.driver:neo4j-java-driver:4.4.0 或更高版本)。

对于命令式和响应式版本,您需要分别提供一个 UserSelectionProvider 或一个 ReactiveUserSelectionProvider。同一个实例需要传递给 Neo4ClientNeo4jTransactionManager 或其相应的响应式变体。

不使用 Boot 的命令式不使用 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,您需要完全自定义 Client 和事务管理器。

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 Driver 都不进行 Cypher 解析,并且这两个构建块默认都假定是写操作。做出此决定是为了开箱即用支持所有操作。如果在栈中的某个地方默认假定为只读,则栈可能会将写查询发送到读副本并执行失败。

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

下面介绍一些选项:

使整个 Repository 只读
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
interface PersonRepository extends Neo4jRepository<Person, Long> {
}
使选定的 Repository 方法只读
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 自定义存储过程可以做各种事情,目前我们无法在此处检查是只读还是写入。
从 Service 编排对 Repository 的调用
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 在这里,对多个 Repository 的几次调用被包装在一个单一的只读事务中。
在私有 Service 方法内和/或使用 Neo4j Client 时使用 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 使用事务模板的首要原因:声明式事务在其本质上使用 Aspects 和代理实现,因此无法在包私有或私有方法中工作,也无法在内部方法调用(想象此 Service 中的另一个方法调用 internalOperation)中工作。
3 Neo4jClient 是 SDN 提供的固定工具类。它不能被注解,但它与 Spring 集成。因此,它为您提供了使用纯粹的 Driver 可以做的一切,并且无需自动映射,同时支持事务。它也遵循声明式事务。

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

正如在书签管理中简要提到的,无需配置任何与书签相关的内容。然而,检索 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 此 Supplier 可以是持有您希望引入系统中的最新书签的任何东西。
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。这对于序列生成可能很有用。

非 final 字段的 ID 不需要 Setter 方法。

我必须为每个领域类创建一个 Repository 吗?

不需要。请查看 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> 的 Repository 方法?

虽然对于返回 Page<T>Slice<T> 的派生查询方法,您除了 Pageable 参数外无需提供其他任何内容,但您必须准备自定义查询以处理 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。您应该使用已排序的 Pageable。
2 此方法使用 @Query 定义自定义查询。它返回一个 Slice<Person>。Slice 不知道总页数,因此自定义查询不需要单独的计数查询。SDN 会通知您它正在估算下一个切片。Cypher 模板必须包含 $skip$limit 这两个 Cypher 参数。如果省略它们,SDN 将发出警告。结果可能与您的预期不符。此外,Pageable 应该未排序,并且您应该提供一个稳定的顺序。我们不会使用来自 Pageable 的排序信息。
3 此方法返回一个 Page。Page 知道总页数的精确数量。因此,您必须指定一个额外的计数查询。第二种方法的所有其他限制也适用。

我可以映射命名路径吗?

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

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

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

“培根”距离
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;
}

当对类型为 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 这样的关系类型,并且它们也在领域模型中存在,则这些关系将根据路径进行填充。

当您使用基于路径查询水化(hydrated)的节点来保存数据时,请特别注意。如果不是所有的关系都被水化,数据将会丢失。

反之亦然。相同的查询可以用于 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 函数返回)。

命名路径可以有效地用于填充和返回不仅仅是根节点,请参阅 附录/自定义查询.adoc#自定义查询.路径

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

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

因此,请在结果形状与您的领域模型一致,或者您确信您不会使用部分映射的模型进行写入操作的所有情况下,使用 Repository 和带有 @Query 的声明式方法。

还有哪些替代方案?

  • 投影(Projections)可能已经足够塑造您对图的“视图”:它们可以用来以明确的方式定义属性和相关实体的抓取深度:通过对它们进行建模。

  • 如果您的目标是只让查询的条件变得动态,那么请查看 QuerydslPredicateExecutor,特别是我们自己的变体 CypherdslConditionExecutor。这两个Mixin都允许将条件添加到我们为您创建的完整查询中。因此,您将拥有完全填充的领域模型以及自定义条件。当然,您的条件必须与我们生成的内容兼容。在此处查找根节点、相关节点等的名称。

  • 通过 CypherdslStatementExecutorReactiveCypherdslStatementExecutor 使用 Cypher-DSL。Cypher-DSL 注定是用于创建动态查询的。最终,它也是 SDN 在底层使用的东西。相应的 Mixin 既可以与 Repository 自身的领域类型一起工作,也可以与投影一起工作(这是添加条件的 Mixin 不具备的功能)。

如果您认为可以通过部分动态查询或完全动态查询以及投影来解决您的问题,那么现在请跳回到 关于 Spring Data Neo4j Mixin 的章节。

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

为什么现在要谈论自定义 Repository 片段?

  • 您可能遇到更复杂的情况,需要不止一个动态查询,但这些查询在概念上仍属于 Repository 而非 Service 层

  • 您的自定义查询返回一个图形状的结果,这与您的领域模型不太匹配,因此自定义查询应伴随自定义映射

  • 您需要与 Driver 交互,例如用于不应通过对象映射进行的批量加载。

假设以下 Repository 声明,它基本上聚合了一个基本 Repository 外加 3 个片段:

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

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

该 Repository 包含 Movies,如入门章节中所示。

Repository 扩展的附加接口(DomainResults, NonDomainResultsLowlevelInteractions)就是解决上述所有问题的片段。

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

片段 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 而非 Driver 的好处是:

  • 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 Client 只接受字符串,但在渲染为字符串时仍然可以使用 Cypher-DSL。
3 将单个值绑定到命名参数。还有一个重载可以绑定整个参数 Map。
4 这是您想要的结果类型
5 最后,是 mappedBy 方法,它暴露结果中的每一条记录以及(如果需要)Driver 的类型系统。这是您钩入进行自定义映射的 API。

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

低层交互

有时您可能希望从 Repository 执行批量加载或删除整个子图,或者以非常特定的方式与 Neo4j Java-Driver 交互。这也是可能的。以下示例展示了如何操作:

使用纯 Driver 的片段
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 会自动拾取所有这三个接口和实现。无需进一步配置。此外,只需增加一个额外的片段(定义所有三个方法的接口)和一个实现,就可以创建相同的整体仓库。该实现将注入所有这三个抽象(template、client 和 driver)。

当然,所有这些也适用于响应式仓库。它们将与 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 基础类需要这个签名。获取 Neo4jOperationsNeo4jTemplate 的实际规范)和实体信息,并在需要时将其存储在属性中。

在这个例子中,我们禁止使用 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。Spring Data Neo4j 将在实体持久化之前立即拾取这些 bean 并按顺序调用它们(如果它们实现了 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.Neo4jRepositoryorg.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.5</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 client、模板以及对命令式仓库的支持,请使用此处所示的类似方法

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

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

import org.springframework.transaction.annotation.EnableTransactionManagement;

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

@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractNeo4jConfig {

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

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

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

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

以下列表提供了响应式 Neo4j client 和模板,启用响应式事务管理并发现与 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://localhost: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 Driver 的带注解类型

Neo4j Java Driver 的 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 提供的容器),您可以这样启用扩展

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