此版本仍在开发中,目前尚不被视为稳定版本。如需最新稳定版本,请使用 Spring Data Neo4j 8.0.4spring-doc.cadn.net.cn

FAQ

Neo4j-OGM 是一个对象图映射(Object Graph Mapping)库,主要被早期版本的 Spring Data Neo4j 用作其后端,以承担将节点和关系映射到领域对象的繁重工作。
当前的 SDN 不需要不支持 Neo4j-OGM。
SDN 仅使用 Spring Data 的映射上下文来扫描类并构建元模型。spring-doc.cadn.net.cn

虽然这将软件定义网络(SDN)与Spring生态系统绑定,但它具有多项优势,其中包括更小的CPU和内存占用开销,尤其是Spring映射上下文的所有功能。spring-doc.cadn.net.cn

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

SDN 具有 SDN+OGM 中不存在的若干特性,特别是spring-doc.cadn.net.cn

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

SDN 是否支持嵌入式 Neo4j?

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

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

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

No.
嵌入式数据库通常由 org.neo4j.graphdb.GraphDatabaseService 的实例表示,且默认情况下不提供 Bolt 连接器。spring-doc.cadn.net.cn

SDN 可以与 Neo4j 的测试工具包进行大量协作,该测试工具包专门设计为真实数据库的即插即用替代方案。驱动程序的 Spring Boot Starters 实现了对 Neo4j 3.5、4.x 和 5.x 测试工具包的支持。请查看对应的模块 org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigurespring-doc.cadn.net.cn

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

SDN 依赖 Neo4j Java 驱动程序。每个 SDN 版本均使用与发布时可用的最新 Neo4j 兼容的 Neo4j Java 驱动程序版本。虽然 Neo4j Java 驱动程序的小版本通常可直接替换,但 SDN 会确保即使在次要版本之间也能互换,因为它会在必要时检查方法或接口是否存在或缺失的变化。spring-doc.cadn.net.cn

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

使用 Spring Boot

如今,Spring Boot 部署是基于 Spring Data 的应用程序最可能的部署方式。请使用 Spring Boot 的依赖管理来更改驱动版本,例如:spring-doc.cadn.net.cn

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

没有 Spring Boot

没有 Spring Boot,您将需要手动声明依赖。对于 Maven,我们建议使用如下 <dependencyManagement /> 部分:spring-doc.cadn.net.cn

无需 Spring Boot 即可通过 Maven(pom.xml)更改驱动版本
<dependencyManagement>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
        <version>5.4.0</version>
    </dependency>
</dependencyManagement>

(此内容已忽略。)

你可以通过静态配置数据库名称或者运行你自己的数据库名称提供器。 请注意,SDN 不会为你创建数据库。 你可以借助一个 迁移工具 或者当然也可以在启动前使用一个简单的脚本来完成。spring-doc.cadn.net.cn

静态配置

在你的Spring Boot配置中,像这样指定要使用的数据库名称(当然,对于YML或基于环境的配置也适用,应用了Spring Boot的约定):spring-doc.cadn.net.cn

spring.data.neo4j.database = yourDatabase

有了这个配置,所有 SDN 仓库(包括响应式和命令式)的所有实例以及 ReactiveNeo4jTemplateNeo4jTemplate 生成的所有查询都将针对数据库 yourDatabase 执行。spring-doc.cadn.net.cn

Dynamically 配置的

具有 Neo4jDatabaseNameProviderReactiveDatabaseSelectionProvider 个推荐实例,控制快逐管理实例类型。spring-doc.cadn.net.cn

该bean可以使用Spring的安全上下文来检索租户。下面是一个使用Spring Security进行强制身份验证的应用程序的工作示例:spring-doc.cadn.net.cn

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

@Configuration
public class Neo4jConfig {
	@Bean
	DatabaseSelectionProvider databaseSelectionProvider() {

		return () -> Optional.ofNullable(SecurityContextHolder.getContext())
			.map(SecurityContext::getAuthentication)
			.filter(Authentication::isAuthenticated)
			.map(Authentication::getPrincipal)
			.map(User.class::cast)
			.map(User::getUsername)
			.map(DatabaseSelection::byName)
			.orElseGet(DatabaseSelection::undecided);
	}
}
请注意,不要在一个数据库中混合使用来自另一个数据库的实体。</p> <p>每次请求新的事务时都会获取数据库名称,因此在两次调用之间更改数据库名称可能会导致预期得到的实体少于或多于预期。</p> <p>或者更糟的是,您可能会无意地将错误的实体存储到错误的数据库中。

Spring Boot 的 Neo4j 整合性指示器针对默认数据库,我如何更改它?

Spring Boot 提供了面向过程和响应式 Neo4j 健康指标。两者都可以检测应用程序上下文中的多个 1 bean 并为每个实例提供整体健康状况的贡献。但是,Neo4j 驱动程序会连接到服务器而不是该服务器中的特定数据库。即使没有 Spring Data Neo4j,Spring Boot 也可以配置驱动程序,并且由于要使用哪个数据库的信息与 Spring Data Neo4j 绑定在一起,因此此信息对内置健康指示器不可用。spring-doc.cadn.net.cn

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

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

命令式变体

import java.util.Optional;

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

public class DatabaseSelectionAwareNeo4jHealthIndicator extends AbstractHealthIndicator {

    private final Driver driver;

    private final DatabaseSelectionProvider databaseSelectionProvider;

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

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

            class Tuple {
                String edition;
                ResultSummary resultSummary;

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

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

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

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

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

import java.util.Map;

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

@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {

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

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

    @Bean (3)
    InitializingBean healthContributorRegistryCleaner(
        HealthContributorRegistry healthContributorRegistry,
        Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators
    ) {
        return () -> customNeo4jHealthIndicators.keySet()
            .stream()
            .map(HealthContributorNameFactory.INSTANCE)
            .forEach(healthContributorRegistry::unregisterContributor);
    }
}
1 如果要使用多个驱动程序和数据库选择提供程序,您需要为每种组合创建一个指示器
2 This makes sure that all of those indicators are grouped under Neo4j, replacing the default Neo4j health indicator
3 This prevents the individual contributors showing up in the health endpoint directly

Reactive variant

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

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

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

public final class DatabaseSelectionAwareNeo4jReactiveHealthIndicator
    extends AbstractReactiveHealthIndicator {

    private final Driver driver;

    private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;

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

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

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

当然,配置的响应式变体。因为Spring Boot会包装现有的反应性指标,以便与非响应式的监控端点一起使用,所以需要两个不同的注册表清理程序。spring-doc.cadn.net.cn

import java.util.Map;

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

@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {

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

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

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

使用不同用户进行委派 - Neo4j 4.4+ 如何支持它们?

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

该特性需要服务器端的 Neo4j Enterprise 4.4+ 和客户端驱动程序的 4.4+(0 或更高)。spring-doc.cadn.net.cn

对于 imperative 和 reactive 版本,都需要提供一个 UserSelectionProvider 和一个 ReactiveUserSelectionProvider。相同的实例需要传递到 Neo4ClientNeo4jTransactionManager,或它们的 reactive 变体。spring-doc.cadn.net.cn

在<断行符>Bootless imperative断行符><断行符>和<断行符>reactive断行符>配置中,你只需要提供该断行符>指定类型的断行符>bean断行符>。断行符>spring-doc.cadn.net.cn

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

public class CustomConfig {

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

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

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

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

import org.springframework.transaction.PlatformTransactionManager;

public class CustomConfig {

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

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

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

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

使用Spring Data Neo4j从Neo4j集群实例

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

是否需要特定配置才能让事务在Neo4j有主群集中正常工作?

无需,您无需。SDN 本身内部使用 Neo4j 因果集群书签,没有任何需要您配置的地方。在同一个线程或同一个反应流中连续执行的事务将能够读取其之前所做的更改,正如您所期望的那样。spring-doc.cadn.net.cn

是否应该为Neo4j集群使用只读事务?

是的,就是这样。Neo4j 集群体系结构是一种因果集群体系结构,它区分主服务器和辅助服务器。主服务器可以是单个实例或核心实例。两者都可以处理读写操作。写操作从核心实例传播到读副本或一般情况下,传播到群集中的追随者。这些追随者是辅助服务器。辅助服务器不处理写操作。spring-doc.cadn.net.cn

在标准部署场景中,您将拥有某些核心实例和群集内的许多只读副本。因此,在以这种方式扩展群集时,重要的是将操作或查询标记为只读,以便领导者永远不会不堪重负,并且尽可能多地传播查询。spring-doc.cadn.net.cn

Spring Data Neo4j 以及底层的 Java 驱动都不进行 Cypher 解析,这两个构建块默认都假设执行写操作。这一决定是为了支持所有操作一应俱全。如果某个组件默认假设为只读,该堆栈可能会向只读复制节点发送写查询,并在执行时失败。spring-doc.cadn.net.cn

All findById, findAllById, findAll and predefined existential methods are marked as read-only by default.

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

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

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

interface PersonRepository extends Neo4jRepository<Person, Long> {

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

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

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

interface PersonRepository extends Neo4jRepository<Person, Long> {
}

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

public class PersonService {

  private final PersonRepository personRepository;
  private final MovieRepository movieRepository;

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

  @Transactional(readOnly = true)
  public Optional<PersonDetails> getPerson(Long id) { (1)
    return this.repository.findById(id)
      .map(person -> {
        var movies = this.movieRepository
          .findByLikedByPersonName(person.getName());
        return new PersonDetails(person, movies);
            });
    }
}
1 在这里,多个存储库调用被包装在一个单一的只读事务中。
在私有服务方法和/或使用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 该值为固定的实用程序,由 SDN 提供。它不能被注释,但它与 Spring 集成。 因此,您可以执行与纯驱动程序相同的操作,并且无需自动映射,也能实现事务处理。 它还遵守声明性事务处理。

可以检索最新的书签或对事务管理器进行种子处理吗?

如在 书签管理 中简要提及,无需为书签配置任何内容。不过,从数据库检索 SDN 事务系统从外部接收的最新书签可能很有用。您可以通过添加 @Bean 来实现此目的,如下所示 BookmarkCapturespring-doc.cadn.net.cn

BookmarkCapture.java
import java.util.Set;

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

public final class BookmarkCapture
    implements ApplicationListener<Neo4jBookmarksUpdatedEvent> {

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

对于种子交易系统,需要像下面这样的自定义事务管理器。spring-doc.cadn.net.cn

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

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

@Configuration
public class BookmarkSeedingConfig {

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

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

        Neo4jBookmarkManager bookmarkManager =
            Neo4jBookmarkManager.create(bookmarkSupplier); (3)
        return new Neo4jTransactionManager(
            driver, databaseNameProvider, bookmarkManager); (4)
    }
}
1 让 Spring 进行注入
2 这个提供商可以是任何能够持有您希望带入系统的最新书签的任何东西
3 使用它创建书签管理器
4 将其传递给自定义事务管理器
no需要做上述任何一件事,除非您的应用程序需要访问或提供 此数据。如果有疑问,就不要做。

可以禁用书签管理吗?

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

使用此书签管理器,风险自担。它会有效地禁用任何书签管理,通过删除所有书签且从不提供任何书签来实现。在群集中,您将面临极高的 stale reads 风险。在一个实例中,这很可能不会有任何影响。

在集群中,这种方式只有在可以接受 stale reads(滞留读)且不担心 overwrite 旧数据的情况下才合理。spring-doc.cadn.net.cn

此配置创建了一个无操作的书签管理器变体,该变体将从相应的类中被拾取。spring-doc.cadn.net.cn

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

@Configuration
public class BookmarksDisabledConfig {

    @Bean
    public Neo4jBookmarkManager neo4jBookmarkManager() {

        return Neo4jBookmarkManager.noop();
    }
}

您可以单独配置Neo4jTransactionManager/Neo4jClientReactiveNeo4jTransactionManager/ReactiveNeo4jClient的配对,但在仅在需要为特定的数据库选择进行配置时才建议这样做。spring-doc.cadn.net.cn

是否需要使用Neo4j特定的注解?

No.、您可以有材材采用下列統算定义的Spring Data注解:spring-doc.cadn.net.cn

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

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

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

Marks the annotated attribute as the unique id.spring-doc.cadn.net.cn

Specific annotation has no additional features.spring-doc.cadn.net.cn

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

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

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

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

如何使用已分配的id?

使用@Id而不使用@GeneratedValue,通过构造函数参数、setter方法或wither填充你的id属性。有关查找良好id的一般性评论,请参见此博客文章spring-doc.cadn.net.cn

如何使用外部生成的 id?

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

ThingWithGeneratedId.java
@Node
public class ThingWithGeneratedId {

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

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

ThingWithIdGeneratedByBean.java
@Node
public class ThingWithIdGeneratedByBean {

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

随着上述内容,idGeneratingBean 指的是 Spring 上下文中的一个 bean。 这可能在序列生成时很有用。spring-doc.cadn.net.cn

Setters are not required on non-final fields for the id.

是否需要为每个域类创建存储库?

编号。 查看SDN构建块并找到Neo4jTemplateReactiveNeo4jTemplatespring-doc.cadn.net.cn

这些模板了解您的域,并为检索、编写和计算实体提供所有必要的基本 CRUD 方法。spring-doc.cadn.net.cn

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

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

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
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;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;

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

@Neo4jIntegrationTest
@DataNeo4jTest
public class TemplateExampleTest {

	@Test
	void shouldSaveAndReadEntities(@Autowired Neo4jTemplate neo4jTemplate) {

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

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

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

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

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

}

并专颈父笔, 缺紞练习:spring-doc.cadn.net.cn

ReactiveTemplateExampleTest.java
import java.util.Collections;

import org.junit.jupiter.api.Test;
import org.testcontainers.neo4j.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.test.StepVerifier;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
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;

@Testcontainers
@DataNeo4jTest
class ReactiveTemplateExampleTest {

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

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

	@Test
	void shouldSaveAndReadEntities(@Autowired ReactiveNeo4jTemplate neo4jTemplate) {

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

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

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

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

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

}

请记住,两个示例都使用了从Spring Boot的@DataNeo4jTestspring-doc.cadn.net.cn

如何使用自定义查询与返回特定类型的仓库方法Page<T> or Slice<T>?

在派生查找方法返回Page<T>Slice<T>时(表示单个或多个结果),除了可以传入Pageable作为参数外,您必须准备好自定义查询以处理分页信息。 Pages and Slices提供了需要的概览。spring-doc.cadn.net.cn

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

public interface MyPersonRepository extends Neo4jRepository<Person, Long> {

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

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

    @Query(
    	value = ""
            + "MATCH (n:Person) WHERE n.name = $name RETURN n "
            + "ORDER BY n.name ASC SKIP $skip LIMIT $limit",
        countQuery = ""
            + "MATCH (n:Person) WHERE n.name = $name RETURN count(n)"
    )
    Page<Person> findPageByName(String name, Pageable pageable); (3)
}
1 通过为您创建查询来派生查找方法。它为您处理Pageable。你应该使用分页器。
2 此方法使用@Query来定义自定义查询。它返回一个Slice<Person>

切片不知道页面总数,所以自定义查询不需要专用计数查询。SDN 将通知您它会估计下一片段。

Cypher 模板必须包含两个$skip$limit Cypher 参数。

如果省略它们,SDN 将发出警告。这可能会与您的预期不符。

而且Pageable应该没有排序,并且您必须提供一个稳定的顺序。

我们将不使用 pageable 提供的排序信息。
3 此方法返回一个页面。页面知道总页数的确切数量。 因此,您必须指定一个额外的计数查询。 第二个方法的所有其他限制均适用。

可以映射命名路径吗?

A series of connected nodes and relationships is called a "path" in Neo4j.spring-doc.cadn.net.cn

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

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

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

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

image$bacon distance

我们找到了3个标记为Vertex的节点和2个标记为Movie的节点。都可以用自定义查询映射。spring-doc.cadn.net.cn

spring-doc.cadn.net.cn

假设对VertexMovieActor也有节点实体来处理关系:spring-doc.cadn.net.cn

spring-doc.cadn.net.cn

常规电影图域模型
@Node
public final class Person {

	@Id @GeneratedValue
	private final Long id;

	private final String name;

	private Integer born;

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

@RelationshipProperties
public final class Actor {

	@RelationshipId
	private final Long id;

	@TargetNode
	private final Person person;

	private final List<String> roles;
}

@Node
public final class Movie {

	@Id
	private final String title;

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

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

当如在"Bacon"距离中所示使用查询,对于类型为Vertex的领域类像这样使用时spring-doc.cadn.net.cn

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

它将从路径中检索所有人员并进行映射。
如果路径上存在关系类型,这些类型也存在于域中,
则会相应地填充这些信息。
spring-doc.cadn.net.cn

在使用从路径查询中水合的节点保存数据时,请特别注意。如果未完全装填关系,数据将丢失。

相反的情况也可以。同一条查询语句可以用Movie实体。它只填充电影。下面的清单显示了如何这样做以及如何用不需要在路径上找到的数据充实查询。这些数据用于正确地填充缺失的关系(在该情况下,所有演员)spring-doc.cadn.net.cn

interface MovieRepository extends Neo4jRepository<Movie, String> {

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

查询返回路径加上所有关系及相关的节点,从而完全"水合"(hydrated)电影实体。spring-doc.cadn.net.cn

The path mapping works for single paths as well for multiple records of paths (which are returned by the allShortestPath function.)spring-doc.cadn.net.cn

命名路径可以高效地用于填充和返回根节点之外的更多内容,参见 附录自定义查询.adoc#自定义查询.paths

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

不,@Query 并不是运行自定义查询的唯一方式。 注解在自定义查询完全填充了你的领域模型时使用最为方便。 请记住,SDN 假设你映射的领域模型是事实。 这意味着如果你通过 @Query 使用的自定义查询只部分填充了模型,你就可能在同一对象上写回数据,这最终会覆盖或擦除你尚未考虑在查询中考虑的数据。spring-doc.cadn.net.cn

所以,請在所有結果呈現為您的域模型或您確定不使用部分映射模型用於寫入命令的情況下,始終在所有情況下使用存儲庫和宣告式方法。spring-doc.cadn.net.cn

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

  • 投影(Projections) 可能已经足够来塑造您对图的 视图(view):它们可以用显式的方式来定义属性和相关实体的获取深度:通过建模它们。要查看这些投影,请在浏览器中打开该页面,然后单击上面的链接之一以访问每个投影的详细信息。spring-doc.cadn.net.cn

  • 如果您的目标只是使查询的条件为动态的,那么请看一下我们提供的QuerydslPredicateExecutor以及我们的变体CypherdslConditionExecutor。这两个xref page允许为您创建的完整查询添加条件。因此,您将与自定义条件一起完全填充域。当然,您的条件必须与我们生成的内容兼容。查找根节点、相关节点和其他名称的xref page,请单击appendix/custom-queries.html#custom-queriesspring-doc.cadn.net.cn

  • 使用 Cypher-DSL 通过 CypherdslStatementExecutor 或者 ReactiveCypherdslStatementExecutor。 Cypher-DSL 旨在创建动态查询。 最终,这正是 SDN 在幕后使用的。 相应的混合工作与存储库本身的域类型以及投影(这是添加条件的混合所不具有的)。spring-doc.cadn.net.cn

(如果认为,您可以通过部分动态查询或完整动态查询结合投影来解决您的问题,请现在跳回第章关于Spring数据Neo4j混合)spring-doc.cadn.net.cn

否则,请阅读以下两个方面的内容: 自定义存储库片段 SDN 我们提供的抽象级别spring-doc.cadn.net.cn

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

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

  • 您的自定义查询返回一个图形结果,其格式并不完全符合您的域模型,因此自定义查询应配备相应的自定义映射spring-doc.cadn.net.cn

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

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

A repository composed of several fragments
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;

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

}

The repository contains Movies as shown in the getting started section.spring-doc.cadn.net.cn

存储库从其扩展的其他接口(DomainResultsNonDomainResultsLowlevelInteractions)是解决所有上述问题的片段。spring-doc.cadn.net.cn

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

片段 DomainResults 声明了一个额外的方法 findMoviesAlongShortestPathspring-doc.cadn.net.cn

DomainResults 片段
import java.util.List;

import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
import org.springframework.transaction.annotation.Transactional;

interface DomainResults {

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

}

此方法使用 @Transactional(readOnly = true) 注解,以表明读者可以回答它。 它不能由 SDN 推导出来,而是需要一个自定义查询。 该自定义查询由该接口的一个实现提供。 这个实现具有相同的名称,后缀为 Implspring-doc.cadn.net.cn

使用 Neo4jTemplate 的片段实现
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.neo4j.cypherdsl.core.Cypher;

import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;

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;

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 = Cypher.shortestK(1).named("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 this.neo4jTemplate.findAll(statement, parameters, MovieEntity.class); (2)
    }

}
1 The Neo4jTemplate is injected by the runtime through the constructor of DomainResultsImpl. No need for @Autowired.
2 Cypher-DSL 用于构建复杂语句(与 路径映射 中显示的内容非常相似)。)

spring-doc.cadn.net.cn

该语句可以直接传递给模板。spring-doc.cadn.net.cn

<para>模板有针对基于String查询的重载,因此您也可以作为String写入查询。</para> <para>这里的重要收获是:</para>spring-doc.cadn.net.cn

<span>使用自定义查询和自定义映射</span>

经常情况下,自定义查询意味着需要自定义结果。应该将所有这些结果都映射为 @Node 吗?当然不是!很多时候,这些对象表示只读命令,并且不打算用作写命令。使用 SDN 时,可能无法或不愿意映射 Cypher 中的所有内容。不过,它提供了几个钩子来运行你自己的映射:在 Neo4jClient。使用 SDN Neo4jClient 的好处超过驱动程序:spring-doc.cadn.net.cn

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

一个声明非域类型结果的片段
import java.util.Collection;

import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.transaction.annotation.Transactional;

interface NonDomainResults {

    @Transactional(readOnly = true)
    Collection<Result> findRelationsToMovie(MovieEntity movie); (1)

    class Result {

        (2)
        public final String name;

        public final String typeOfRelation;

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

    }

}
1 This is a made up non-domain result. A real world query result would probably look more complex.
2 此片段添加的方法。同样,该方法使用 Spring 的 @Transactional 进行注解。

没有该片段的实现,启动会失败,所以下面是它的实现:spring-doc.cadn.net.cn

A使用Neo4jClient的片段实现
import java.util.Collection;

import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;

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 客户端只接受String,但当呈现为String时,仍然可以使用Cypher-DSL
3 将单个值绑定到命名参数。还有重载来绑定整个参数映射
4 这是你期望的结果类型
5 最终,是mappedBy方法,为结果中的每个条目暴露一个Record,如果需要的话再加上驱动程序的类型系统。 这是你为自定义映射进行钩入的API

整个查询在一个Spring事务的上下文中运行,在这种情况下,是一个只读事务。spring-doc.cadn.net.cn

低级交互

有时,您可能需要从存储库批量加载数据,或删除整个子图,或者以非常特定的方式与Neo4j Java驱动程序进行交互。这也是可行的。下面的例子展示了如何做到这一点:spring-doc.cadn.net.cn

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

    int deleteGraph();

}


import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.neo4j.driver.summary.SummaryCounters;

class LowlevelInteractionsImpl implements LowlevelInteractions {

    private final Driver driver; (1)

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

    @Override
    public int deleteGraph() {

        try (Session session = this.driver.session()) {
            SummaryCounters counters = session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()) (2)
                .counters();
            return counters.nodesDeleted() + counters.relationshipsDeleted();
        }
    }

}
1 直接与驱动程序一起工作。如同所有示例一样:不需要@Autowired魔法。所有的片段实际上都可以单独测试。
2 用例是虚构的。在这里,我们使用驱动程序管理的事务来删除整个图,并返回已删除节点和关系的数量

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

把所有内容整合在一起,这个测试成功了:spring-doc.cadn.net.cn

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

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

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

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

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

最后总结一下:Spring Data Neo4j会自动选择这三个接口和实现类。 无需进一步配置。 同样,可以通过仅添加一个额外片段(定义所有三个方法的接口) 和一个实现类来创建相同的总体仓库。 该实现类将注入所有三个抽象(模板、客户端和驱动程序)。spring-doc.cadn.net.cn

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

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

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

基本上与共享的Spring Data Commons文档中为Spring Data JPA显示的方式相同,参见自定义基本存储库
只是在我们的情况下,您将扩展从
spring-doc.cadn.net.cn

自定义基础仓库
public static 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方法。您可以添加采用fetch深度的方法,并基于该depth运行自定义查询。一种实现方式如DomainResults片段所示。spring-doc.cadn.net.cn

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

如何审核实体?

所有Spring Data注解都受支持。<br/>这些 are<br/>spring-doc.cadn.net.cn

审计 向您展示了如何在 Spring Data Commons 的更大背景下使用审计。
下面的列表显示了 Spring Data Neo4j 提供的所有配置选项:spring-doc.cadn.net.cn

启用和配置Neo4j审核
@Configuration
@EnableNeo4jAuditing(modifyOnCreate = false, (1)
        auditorAwareRef = "auditorProvider", (2)
        dateTimeProviderRef = "fixedDateTimeProvider" (3)
)
class AuditingConfig {

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

    @Bean
    DateTimeProvider fixedDateTimeProvider() {
        return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE);
    }

}
1 如果希望在创建时也写入修改数据,请将其设置为 true
2 使用此属性指定提供审计员(即用户名)的bean名称
3 使用此属性指定提供当前日期的 bean 的名称。在这种情况下,由于上述配置是我们测试的一部分,因此使用固定日期

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

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

它们可以修改实体或返回一个全新的实体。<br/>下面的例子向上下文中添加了一个回调,该回调在实体被持久化之前更改了一个属性:spring-doc.cadn.net.cn

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

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;
import org.springframework.data.neo4j.integration.shared.common.ThingWithAssignedId;

@Configuration
class CallbacksConfig {

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

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

}

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

如何使用“根据示例查找”功能?

"Find by example" 是 SDN 中的新特性。 您可以实例化一个实体或使用现有的实体。 使用该实例,您可以创建一个 org.springframework.data.domain.Example。 如果您的仓库扩展了 org.springframework.data.neo4j.repository.Neo4jRepositoryorg.springframework.data.neo4j.repository.ReactiveNeo4jRepository,您可以立即使用带有示例参数的可用 findBy 方法,如在 findByExample 中所示。spring-doc.cadn.net.cn

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

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

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

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

使用 Spring Data Neo4j 是否需要 Spring Boot?

不需要,你不必。 尽管通过 Spring Boot 自动配置许多 Spring 方面可以消除大量的手动繁琐工作,并且是设置新 Spring 项目的推荐方法,但你并不需要使用这种方法。spring-doc.cadn.net.cn

上述解决方案所需的依赖项如下:spring-doc.cadn.net.cn

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

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

要选择不同的数据库——无论是静态还是动态——您可以添加类型为 DatabaseSelectionProvider 的 Bean,如Neo4j 4 支持多个数据库 - 如何使用它们? 所述。
对于响应式场景,我们提供 ReactiveDatabaseSelectionProviderspring-doc.cadn.net.cn

在不使用 Spring Boot 的情况下,在 Spring 环境中使用 Spring Data Neo4j

我们提供了两个抽象配置类,帮助您引入必要的 bean:org.springframework.data.neo4j.config.AbstractNeo4jConfig用于命令式数据库访问和org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig用于响应式版本。它们分别与@EnableNeo4jRepositories@EnableReactiveNeo4jRepositories一起使用。参见启用Spring Data Neo4j基础设施进行命令式数据库访问启用Spring Data Neo4j基础设施进行响应式数据库访问以获取示例用法。这两个类都需要您重写driver(),在此创建驱动程序。spring-doc.cadn.net.cn

要获取Neo4j 客户端的命令式版本、模板以及对命令式存储库的支持,请使用此处所示的类似方法:spring-doc.cadn.net.cn

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

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

import org.springframework.transaction.annotation.EnableTransactionManagement;

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

@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractNeo4jConfig {

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

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

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

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

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

为响应式数据库访问启用Spring Data Neo4j基础架构
import org.neo4j.driver.Driver;

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

@Configuration
@EnableReactiveNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractReactiveNeo4jConfig {

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

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

在CDI 2.0环境中使用Spring Data Neo4j

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

您需要引入到应用程序中的唯一内容是带有注解且能够生成 Neo4j Java Driver 的类型:spring-doc.cadn.net.cn

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

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

public class Neo4jConfig {

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

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

    @Produces @Singleton
    public DatabaseSelectionProvider getDatabaseSelectionProvider() { (2)
        return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
    }
}
1 与使用纯Spring时相同,参见启用Spring Data Neo4j基础设施以进行命令式数据库访问,但需用相应的CDI基础设施注解标注。
2 这是<强>可选的。但是,如果您运行自定义数据库选择提供程序,则<强调>必须不为此bean指定限定符。

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

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