常見問題

Neo4j-OGM 是一個物件圖對映庫,主要由以前版本的 Spring Data Neo4j 用作將其節點和關係對映到領域物件的後端。當前的 SDN 不需要不支援 Neo4j-OGM。SDN 專門使用 Spring Data 的對映上下文來掃描類和構建元模型。

雖然這使得 SDN 繫結到 Spring 生態系統,但它具有多項優勢,其中包括更小的 CPU 和記憶體佔用,以及特別是 Spring 對映上下文的所有功能。

為什麼我應該優先使用 SDN 而不是 SDN+OGM

SDN 具有 SDN+OGM 中不存在的多個功能,特別是

  • 完全支援 Spring 的響應式機制,包括響應式事務

  • 完全支援 按示例查詢

  • 完全支援完全不可變實體

  • 支援派生查詢方法的所有修飾符和變體,包括空間查詢

SDN 支援透過 HTTP 連線到 Neo4j 嗎?

不支援。

SDN 支援嵌入式 Neo4j 嗎?

嵌入式 Neo4j 具有多方面

SDN 為您的應用程式提供嵌入式例項嗎?

不支援。

SDN 是否直接與嵌入式例項互動?

不。嵌入式資料庫通常由 org.neo4j.graphdb.GraphDatabaseService 例項表示,並且沒有開箱即用的 Bolt 聯結器。

然而,SDN 可以很好地與 Neo4j 的測試套件一起工作,測試套件專門用於替代真實資料庫。對 Neo4j 3.5、4.x 和 5.x 測試套件的支援透過 驅動程式的 Spring Boot Starter 實現。請檢視相應的模組 org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure

可以使用哪個 Neo4j Java 驅動程式以及如何使用?

SDN 依賴於 Neo4j Java 驅動程式。每個 SDN 版本都使用與釋出時最新 Neo4j 相容的 Neo4j Java 驅動程式版本。雖然 Neo4j Java 驅動程式的補丁版本通常是直接替代品,但 SDN 確保即使是次要版本也可以互換,因為它會在必要時檢查方法的存在或缺失或介面更改。

因此,您可以將任何 4.x Neo4j Java 驅動程式與任何 SDN 6.x 版本一起使用,並將任何 5.x Neo4j 驅動程式與任何 SDN 7.x 版本一起使用。

與 Spring Boot 一起使用

如今,Spring Boot 部署是基於 Spring Data 的應用程式最常見的部署方式。請使用 Spring Boot 的依賴管理來更改驅動程式版本,如下所示

從 Maven (pom.xml) 更改驅動程式版本
<properties>
  <neo4j-java-driver.version>5.4.0</neo4j-java-driver.version>
</properties>

Or

從 Gradle (gradle.properties) 更改驅動程式版本
neo4j-java-driver.version = 5.4.0

不使用 Spring Boot

不使用 Spring Boot,您只需手動宣告依賴項。對於 Maven,我們建議使用 <dependencyManagement /> 部分,如下所示

不使用 Spring Boot 從 Maven (pom.xml) 更改驅動程式版本
<dependencyManagement>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
        <version>5.4.0</version>
    </dependency>
</dependencyManagement>

Neo4j 4 支援多個數據庫 - 如何使用它們?

您可以靜態配置資料庫名稱,也可以執行自己的資料庫名稱提供程式。請記住,SDN 不會為您建立資料庫。您可以使用 遷移工具 來完成此操作,當然也可以事先使用簡單的指令碼。

靜態配置

在 Spring Boot 配置中配置要使用的資料庫名稱,如下所示(當然,相同的屬性也適用於 YML 或基於環境的配置,並應用 Spring Boot 的約定)

spring.data.neo4j.database = yourDatabase

完成此配置後,所有 SDN 儲存庫例項(包括反應式和命令式)以及 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 驅動程式連線到伺服器而不是該伺服器中的特定資料庫。Spring Boot 能夠在沒有 Spring Data Neo4j 的情況下配置驅動程式,並且由於要使用的資料庫資訊繫結到 Spring Data Neo4j,因此此資訊對內建的健康指示器不可用。

這在許多部署場景中可能不是問題。但是,如果配置的資料庫使用者至少沒有預設資料庫的訪問許可權,則健康檢查將失敗。

這可以透過自定義的 Neo4j 健康貢獻者來緩解,這些貢獻者瞭解資料庫選擇。

命令式變體

import java.util.Optional;

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

public class DatabaseSelectionAwareNeo4jHealthIndicator extends AbstractHealthIndicator {

    private final Driver driver;

    private final DatabaseSelectionProvider databaseSelectionProvider;

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

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

            class Tuple {
                String edition;
                ResultSummary resultSummary;

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

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

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

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

這使用可用的資料庫選擇來執行 Boot 執行的相同查詢,以檢查連線是否健康。使用以下配置來應用它

import java.util.Map;

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

@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {

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

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

    @Bean (3)
    InitializingBean healthContributorRegistryCleaner(
        HealthContributorRegistry healthContributorRegistry,
        Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators
    ) {
        return () -> customNeo4jHealthIndicators.keySet()
            .stream()
            .map(HealthContributorNameFactory.INSTANCE)
            .forEach(healthContributorRegistry::unregisterContributor);
    }
}
1 如果您有多個驅動程式和資料庫選擇提供程式,則需要為每個組合建立一個指示器
2 這確保所有這些指示器都分組在 Neo4j 下,替換預設的 Neo4j 健康指示器
3 這阻止了單個貢獻者直接顯示在健康端點中

響應式變體

響應式變體基本相同,使用響應式型別和相應的響應式基礎設施類

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

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

public final class DatabaseSelectionAwareNeo4jReactiveHealthIndicator
    extends AbstractReactiveHealthIndicator {

    private final Driver driver;

    private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;

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

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

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

當然,還有配置的響應式變體。它需要兩個不同的登錄檔清理器,因為 Spring Boot 會將現有的響應式指示器包裝起來,以便與非響應式 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+ 支援模擬不同使用者 - 如何使用?

使用者模擬在大型多租戶環境中尤其有趣,在這種環境中,一個物理連線(或技術)使用者可以模擬許多租戶。根據您的設定,這將大大減少所需的物理驅動程式例項數量。

此功能要求伺服器端為 Neo4j Enterprise 4.4+,客戶端為 4.4+ 驅動程式(org.neo4j.driver:neo4j-java-driver:4.4.0 或更高版本)。

對於命令式和響應式版本,您都需要提供一個 UserSelectionProviderReactiveUserSelectionProvider。相同的例項需要傳遞給 Neo4ClientNeo4jTransactionManager,或者它們的響應式變體。

無 Boot 命令式響應式 配置中,您只需提供一個相關型別的 bean

使用者選擇提供程式 bean
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.UserSelectionProvider;

public class CustomConfig {

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

在典型的 Spring Boot 場景中,此功能需要更多工作,因為 Boot 也支援沒有此功能的 SDN 版本。因此,給定 使用者選擇提供程式 bean 中的 bean,您將需要完全自定義客戶端和事務管理器

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

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

import org.springframework.transaction.PlatformTransactionManager;

public class CustomConfig {

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

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

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

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

從 Spring Data Neo4j 使用 Neo4j 叢集例項

以下問題也適用於 Neo4j AuraDB 以及 Neo4j 本地叢集例項。

我需要特定的配置才能使事務與 Neo4j 因果叢集無縫工作嗎?

不需要。SDN 內部使用 Neo4j 因果叢集書籤,無需您進行任何配置。在同一執行緒或同一響應式流中彼此跟隨的事務將能夠按預期讀取其先前更改的值。

對於 Neo4j 叢集使用只讀事務是否重要?

是的,很重要。Neo4j 叢集架構是一種因果叢集架構,它區分主伺服器和輔助伺服器。主伺服器可以是單個例項或核心例項。它們都可以響應讀寫操作。寫操作從核心例項傳播到叢集內部的讀副本或更一般地說,追隨者。這些追隨者是輔助伺服器。輔助伺服器不響應寫操作。

在標準部署場景中,您將擁有一些核心例項和叢集中的許多讀副本。因此,將操作或查詢標記為只讀以這種方式擴充套件叢集非常重要,以確保領導者永不超負荷,並且查詢儘可能多地傳播到讀副本。

Spring Data Neo4j 和底層 Java 驅動程式都不進行 Cypher 解析,並且兩個構建塊預設都假定寫操作。做出此決定是為了支援所有開箱即用的操作。如果堆疊中的某些部分預設假定只讀,則堆疊可能會最終將寫查詢傳送到讀副本並執行失敗。

所有 findByIdfindAllByIdfindAll 和預定義的存在性方法預設都標記為只讀。

以下描述了一些選項

使整個儲存庫只讀
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.transaction.annotation.Transactional;

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

interface PersonRepository extends Neo4jRepository<Person, Long> {

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

  @Transactional(readOnly = true)
  @Query("""
    CALL apoc.search.nodeAll('{Person: "name",Movie: ["title","tagline"]}','contains','her')
    YIELD node AS n RETURN n""")
  Person findByCustomQuery(); (2)
}
1 為什麼這不是預設只讀的?雖然它適用於上面的派生查詢器(我們實際上知道它是隻讀的),但我們經常看到使用者新增自定義 @Query 並透過 MERGE 構造實現它,這當然是一個寫操作。
2 自定義過程可以執行各種操作,目前我們無法在此處檢查只讀與寫入。
從服務編排對儲存庫的呼叫
import java.util.Optional;

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

interface PersonRepository extends Neo4jRepository<Person, Long> {
}

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

public class PersonService {

  private final PersonRepository personRepository;
  private final MovieRepository movieRepository;

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

  @Transactional(readOnly = true)
  public Optional<PersonDetails> getPerson(Long id) { (1)
    return this.repository.findById(id)
      .map(person -> {
        var movies = this.movieRepository
          .findByLikedByPersonName(person.getName());
        return new PersonDetails(person, movies);
            });
    }
}
1 在這裡,對多個儲存庫的多個呼叫被包裝在一個單獨的只讀事務中。
在私有服務方法內部和/或與 Neo4j 客戶端一起使用 Spring 的 TransactionTemplate
import java.util.Collection;

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

public class PersonService {

  private final TransactionTemplate readOnlyTx;

  private final Neo4jClient neo4jClient;

  public PersonService(PlatformTransactionManager transactionManager, Neo4jClient neo4jClient) {

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

  void internalOperation() { (2)

    Collection<Node> nodes = this.readOnlyTx.execute(state -> {
      return neo4jClient.query("MATCH (n) RETURN n").fetchAs(Node.class) (3)
          .mappedBy((types, record) -> record.get(0).asNode())
          .all();
    });
  }
}
1 使用您需要的特性建立 TransactionTemplate 例項。當然,這也可以是一個全域性 bean。
2 使用事務模板的第一個原因是:宣告式事務在包私有或私有方法中不起作用,在內部方法呼叫中也不起作用(想象一下此服務中的另一個方法呼叫 internalOperation),因為它們透過 Aspect 和代理實現。
3 Neo4jClient 是 SDN 提供的固定實用程式。它不能被註解,但它與 Spring 整合。因此,它為您提供了純驅動程式所能做的一切,無需自動對映和事務。它也遵守宣告式事務。

我可以檢索最新的書籤或為事務管理器播種嗎?

正如 書籤管理 中簡要提及的,無需配置任何與書籤相關的內容。然而,檢索 SDN 事務系統從資料庫接收到的最新書籤可能很有用。您可以新增一個像 BookmarkCapture 這樣的 @Bean 來實現此目的

BookmarkCapture.java
import java.util.Set;

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

public final class BookmarkCapture
    implements ApplicationListener<Neo4jBookmarksUpdatedEvent> {

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

為了為事務系統播種,需要一個定製的事務管理器,如下所示

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

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

@Configuration
public class BookmarkSeedingConfig {

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

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

        Neo4jBookmarkManager bookmarkManager =
            Neo4jBookmarkManager.create(bookmarkSupplier); (3)
        return new Neo4jTransactionManager(
            driver, databaseNameProvider, bookmarkManager); (4)
    }
}
1 讓 Spring 注入這些
2 此供應商可以是任何持有您想要引入系統的最新書籤的物件
3 使用它建立書籤管理器
4 將其傳遞給定製的事務管理器
除非您的應用程式需要訪問或提供此資料,否則無需執行上述任何操作。如有疑問,請勿執行任何操作。

我可以停用書籤管理嗎?

我們提供了一個 Noop 書籤管理器,它有效地停用了書籤管理。

請自行承擔風險使用此書籤管理器,它將透過丟棄所有書籤且從不提供任何書籤來有效地停用任何書籤管理。在叢集中,您將面臨遇到過時讀取的高風險。在單個例項中,它很可能不會產生任何影響。

+ 在叢集中,只有當您能夠容忍過時讀取且沒有覆蓋舊資料的風險時,這才是一種明智的方法。

以下配置建立了一個書籤管理器的“noop”變體,該變體將由相關類獲取。

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

@Configuration
public class BookmarksDisabledConfig {

    @Bean
    public Neo4jBookmarkManager neo4jBookmarkManager() {

        return Neo4jBookmarkManager.noop();
    }
}

您可以單獨配置 Neo4jTransactionManager/Neo4jClientReactiveNeo4jTransactionManager/ReactiveNeo4jClient 對,但我們建議只有當您已經為特定的資料庫選擇需求配置它們時才這樣做。

我需要使用 Neo4j 特定的註解嗎?

不需要。您可以自由使用以下等效的 Spring Data 註解

SDN 特定註解 Spring Data 通用註解 目的 區別

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

org.springframework.data.annotation.Id

將帶註解的屬性標記為唯一 ID。

特定註解沒有額外的功能。

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

org.springframework.data.annotation.Persistent

將類標記為持久化實體。

@Node 允許自定義標籤

我如何使用分配的 ID?

只需使用 @Id 而不使用 @GeneratedValue,並透過建構函式引數或 setter 或 wither 填充您的 ID 屬性。請參閱這篇 部落格文章,瞭解一些關於查詢良好 ID 的一般性說明。

我如何使用外部生成的 ID?

我們提供了介面 org.springframework.data.neo4j.core.schema.IdGenerator。以您想要的方式實現它,並像這樣配置您的實現

ThingWithGeneratedId.java
@Node
public class ThingWithGeneratedId {

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

如果您將類的名稱傳遞給 @GeneratedValue,則此類必須具有無參預設建構函式。但是,您也可以使用字串

ThingWithIdGeneratedByBean.java
@Node
public class ThingWithIdGeneratedByBean {

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

這樣,idGeneratingBean 指的是 Spring 上下文中的一個 bean。這對於序列生成可能很有用。

非 final 欄位的 ID 不需要 setter。

我必須為每個領域類建立儲存庫嗎?

不需要。請檢視 SDN 構建塊 並找到 Neo4jTemplateReactiveNeo4jTemplate

這些模板瞭解您的領域並提供所有必要的基本 CRUD 方法,用於檢索、寫入和計數實體。

這是我們使用命令式模板的典型電影示例

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

}

這是響應式版本,為簡潔起見省略了設定

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

如何將自定義查詢與返回 Page<T>Slice<T> 的儲存庫方法一起使用?

雖然在返回 Page<T>Slice<T> 的派生查詢器方法上,您除了 Pageable 引數之外不需要提供任何其他內容,但您必須準備好自定義查詢來處理可分頁。 頁面和切片 概述了所需內容。

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

public interface MyPersonRepository extends Neo4jRepository<Person, Long> {

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

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

    @Query(
    	value = ""
            + "MATCH (n:Person) WHERE n.name = $name RETURN n "
            + "ORDER BY n.name ASC SKIP $skip LIMIT $limit",
        countQuery = ""
            + "MATCH (n:Person) WHERE n.name = $name RETURN count(n)"
    )
    Page<Person> findPageByName(String name, Pageable pageable); (3)
}
1 一個為您建立查詢的派生查詢器方法。它為您處理 Pageable。您應該使用排序的可分頁。
2 此方法使用 @Query 定義自定義查詢。它返回一個 Slice<Person>。切片不知道總頁數,因此自定義查詢不需要專用的計數查詢。SDN 將通知您它估計了下一個切片。Cypher 模板必須指定 $skip$limit 兩個 Cypher 引數。如果您省略它們,SDN 將發出警告。這可能與您的期望不符。此外,Pageable 應該未排序,並且您應該提供穩定的順序。我們不會使用 Pageable 中的排序資訊。
3 此方法返回一個頁面。頁面知道總頁面的確切數量。因此,您必須指定一個額外的計數查詢。第二種方法的所有其他限制均適用。

我可以對映命名路徑嗎?

在 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),並且這些關係型別也存在於領域中,則它們將從路徑中相應地填充。

當您使用從基於路徑的查詢中水合的節點來儲存資料時,請特別小心。如果並非所有關係都已水合,則資料將丟失。

反之亦然。相同的查詢可以與 Movie 實體一起使用。然後它將只填充電影。以下清單顯示瞭如何做到這一點以及如何用路徑中未找到的附加資料來豐富查詢。這些資料用於正確填充缺失的關係(在這種情況下,是所有演員)

interface MovieRepository extends Neo4jRepository<Movie, String> {

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

查詢返回路徑以及所有收集的關係和相關節點,以便電影實體完全水合。

路徑對映適用於單路徑以及多條路徑記錄(由 allShortestPath 函式返回)。

命名路徑可以有效地用於填充和返回不僅僅是根節點,參見 appendix/custom-queries.adoc#custom-query.paths

@Query 是使用自定義查詢的唯一方式嗎?

不,@Query 不是執行自定義查詢的唯一方法。在自定義查詢完全填充您的域模型的情況下,此註解很方便。請記住,SDN 假定您對映的域模型是真實的。這意味著如果您使用透過 @Query 的自定義查詢僅部分填充模型,則您有使用同一物件回寫資料的風險,這最終將擦除或覆蓋您在查詢中未考慮的資料。

因此,請在結果形狀與您的域模型相同,或者您確定不使用部分對映模型進行寫入命令的所有情況下,使用儲存庫和帶有 @Query 的宣告式方法。

有什麼替代方案?

  • 投影 可能已經足夠塑造您在圖上的檢視:它們可以透過建模來明確定義獲取屬性和相關實體的深度。

  • 如果您的目標是僅使查詢的條件動態化,那麼請檢視 QuerydslPredicateExecutor,尤其是我們自己的變體 CypherdslConditionExecutor。這兩個 混合器 都允許向我們為您建立的完整查詢新增條件。因此,您將擁有完全填充的域以及自定義條件。當然,您的條件必須與我們生成的內容相容。在此處 查詢 根節點、相關節點等的名稱。

  • 透過 CypherdslStatementExecutorReactiveCypherdslStatementExecutor 使用 Cypher-DSL。Cypher-DSL 註定要建立動態查詢。最終,SDN 無論如何都在底層使用它。相應的混合器既適用於儲存庫本身的域型別,也適用於投影(條件混合器不支援此功能)。

如果您認為可以透過部分動態查詢或完全動態查詢與投影結合來解決您的問題,請立即跳回到 關於 Spring Data Neo4j Mixins 的章節。

否則,請閱讀以下兩件事:自定義儲存庫片段 和我們在 SDN 中提供的 抽象級別

為什麼現在要談論自定義儲存庫片段?

  • 您可能遇到更復雜的情況,需要多個動態查詢,但這些查詢在概念上仍屬於儲存庫而非服務層

  • 您的自定義查詢返回的圖結構結果與您的域模型不太匹配,因此自定義查詢也應伴隨自定義對映

  • 您需要與驅動程式互動,例如,進行不應透過物件對映的批次載入。

假設以下儲存庫宣告,它基本上聚合了一個基本儲存庫和 3 個片段

由多個片段組成的儲存庫
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 {

}

儲存庫包含 電影,如 入門部分 所示。

儲存庫擴充套件的附加介面(DomainResultsNonDomainResultsLowlevelInteractions)是解決上述所有問題的片段。

使用複雜的動態自定義查詢但仍返回域型別

片段 DomainResults 聲明瞭一個額外的 findMoviesAlongShortestPath 方法

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 派生,但需要自定義查詢。此自定義查詢由該介面的一個實現提供。該實現具有相同的名稱,字尾為 Impl

使用 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 Neo4jTemplate 由執行時透過 DomainResultsImpl 的建構函式注入。不需要 @Autowired
2 Cypher-DSL 用於構建一個複雜的語句(與 路徑對映 中所示的非常相似)。該語句可以直接傳遞給模板。

該模板也有基於字串的查詢的過載,因此您也可以將查詢寫成字串。這裡重要的啟示是

  • 模板“知道”您的域物件並相應地對映它們

  • @Query 不是定義自定義查詢的唯一選項

  • 它們可以透過各種方式生成

  • @Transactional 註解受到尊重

使用自定義查詢和自定義對映

通常,自定義查詢表示自定義結果。所有這些結果都應該對映為 @Node 嗎?當然不是!很多時候,這些物件代表讀取命令,不打算用作寫入命令。SDN 無法或不希望對映 Cypher 中可能的一切,這也不太可能。但是,它提供了幾個鉤子來執行您自己的對映:在 Neo4jClient 上。使用 SDN Neo4jClient 而不是驅動程式的好處是

  • Neo4jClient 與 Spring 的事務管理整合

  • 它具有用於繫結引數的流暢 API

  • 它具有一個流暢的 API,公開了記錄和 Neo4j 型別系統,以便您可以訪問結果中的所有內容以執行對映

宣告片段與之前完全相同

宣告非域型別結果的片段
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 這是一個虛構的非域結果。實際的查詢結果可能會更復雜。
2 此片段新增的方法。同樣,該方法使用 Spring 的 @Transactional 進行註解

如果沒有該片段的實現,啟動將失敗,所以這裡是

使用 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 客戶端只接受字串,但在渲染為字串時仍然可以使用 Cypher-DSL
3 將一個值繫結到命名引數。還有一個過載可以繫結整個引數對映
4 這是你想要的結果型別
5 最後是 mappedBy 方法,它公開了結果中每個條目的一個 Record,並在需要時公開了驅動程式的型別系統。這是您自定義對映的 API 鉤子

整個查詢在 Spring 事務的上下文中執行,在本例中是一個只讀事務。

低階互動

有時您可能希望從儲存庫進行批次載入,或刪除整個子圖,或以非常特定的方式與 Neo4j Java 驅動程式互動。這也是可能的。以下示例演示瞭如何操作

使用純驅動程式的片段
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。

綜上所述,此測試成功

測試組合儲存庫
@Test
void customRepositoryFragmentsShouldWork(@Autowired PersonRepository people, @Autowired MovieRepository movies) {

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

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

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

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

最後一點:所有三個介面和實現都由 Spring Data Neo4j 自動獲取。無需進一步配置。此外,相同的整體儲存庫可以只用一個附加片段(定義所有三個方法的介面)和一個實現來建立。然後,該實現將注入所有三個抽象(模板、客戶端和驅動程式)。

所有這些當然也適用於響應式儲存庫。它們將與 ReactiveNeo4jTemplateReactiveNeo4jClient 以及驅動程式提供的響應式會話一起工作。

如果您所有儲存庫都有重複方法,則可以替換預設儲存庫實現。

如何使用自定義 Spring Data Neo4j 基本儲存庫?

基本上與共享的 Spring Data Commons 文件中 自定義基本儲存庫 為 Spring Data JPA 顯示的方式相同。只是在我們的例子中,您將從

自定義基本儲存庫
public 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 基類需要此簽名。獲取 Neo4jOperations (Neo4jTemplate 的實際規範) 和實體資訊,並根據需要將其儲存在屬性上。

在此示例中,我們禁止使用 findAll 方法。您可以新增採用獲取深度的方法,並根據該深度執行自定義查詢。一種實現方式如 DomainResults 片段 所示。

要為所有宣告的儲存庫啟用此基本儲存庫,請使用:@EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class) 啟用 Neo4j 儲存庫。

如何審計實體?

所有 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 審計
@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,因此審計器的檢索是響應式流的一部分。

除了這些審計機制之外,您還可以向上下文新增任意數量的實現 BeforeBindCallback<T>ReactiveBeforeBindCallback<T> 的 bean。這些 bean 將被 Spring Data Neo4j 拾取並按順序呼叫(如果它們實現了 Ordered 或使用 @Order 註解),就在實體持久化之前。

它們可以修改實體或返回一個全新的實體。以下示例向上下文添加了一個回撥,該回調在實體持久化之前更改一個屬性

儲存前修改實體
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;
        };
    }

}

不需要額外配置。

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

“按示例查詢”是 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 Boot 才能使用 Spring Data Neo4j 嗎?

不,不需要。雖然透過 Spring Boot 自動配置許多 Spring 方面省去了許多手動繁瑣的工作,並且是設定新 Spring 專案的推薦方法,但您不需要強制使用它。

上述解決方案需要以下依賴項

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

Gradle 設定的座標相同。

要選擇不同的資料庫 - 無論是靜態還是動態 - 您可以新增一個型別為 DatabaseSelectionProvider 的 Bean,如 Neo4j 4 支援多個數據庫 - 如何使用它們? 中所述。對於響應式場景,我們提供 ReactiveDatabaseSelectionProvider

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

我們提供了兩個抽象配置類來幫助您引入必要的 bean:org.springframework.data.neo4j.config.AbstractNeo4jConfig 用於命令式資料庫訪問,org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig 用於響應式版本。它們旨在與 @EnableNeo4jRepositories@EnableReactiveNeo4jRepositories 分別使用。有關示例用法,請參閱 為命令式資料庫訪問啟用 Spring Data Neo4j 基礎設施為響應式資料庫訪問啟用 Spring Data Neo4j 基礎設施。這兩個類都要求您覆蓋 driver(),您應該在該方法中建立驅動程式。

要獲取命令式版本的 Neo4j 客戶端、模板和對命令式儲存庫的支援,請使用此處所示的類似方法

為命令式資料庫訪問啟用 Spring Data Neo4j 基礎設施
import org.neo4j.driver.Driver;

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

import org.springframework.transaction.annotation.EnableTransactionManagement;

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

@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractNeo4jConfig {

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

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

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

        return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
    }
}
1 驅動程式 bean 是必需的。
2 這靜態地選擇了一個名為 yourDatabase 的資料庫,並且是可選的

以下列表提供了響應式 Neo4j 客戶端和模板,啟用響應式事務管理並發現 Neo4j 相關儲存庫

為響應式資料庫訪問啟用 Spring Data Neo4j 基礎設施
import org.neo4j.driver.Driver;

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

@Configuration
@EnableReactiveNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractReactiveNeo4jConfig {

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

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

在 CDI 2.0 環境中使用 Spring Data Neo4j

為了您的方便,我們透過 Neo4jCdiExtension 提供了一個 CDI 擴充套件。當在相容的 CDI 2.0 容器中執行時,它將透過 Java 的服務載入器 SPI 自動註冊和載入。

您只需在應用程式中引入一個帶註解的型別,用於生成 Neo4j Java 驅動程式

Neo4j Java 驅動程式的 CDI 生產者
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;

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

public class Neo4jConfig {

    @Produces @ApplicationScoped
    public Driver driver() { (1)
        return GraphDatabase
            .driver("bolt://: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();
        }
    }
}
© . This site is unofficial and not affiliated with VMware.