常見問題
SDN 與 Neo4j-OGM 有何關聯?
Neo4j-OGM 是一個物件圖形映射函式庫,主要由舊版的 Spring Data Neo4j 使用,作為其後端,負責將節點和關係映射到領域物件的繁重工作。目前的 SDN 不需要 且 不支援 Neo4j-OGM。SDN 專門使用 Spring Data 的映射上下文來掃描類別並建構元模型。
雖然這將 SDN 鎖定在 Spring 生態系統中,但它有幾個優點,其中包括在 CPU 和記憶體使用方面佔用空間較小,尤其是 Spring 映射上下文的所有功能。
為何我應該使用 SDN 而非 SDN+OGM
SDN 具有 SDN+OGM 中沒有的幾項功能,特別是
-
完整支援 Spring 的反應式架構,包括反應式交易
-
完整支援 範例查詢
-
完整支援完全不可變的實體
-
支援所有修飾詞和衍生查詢方法的變體,包括空間查詢
SDN 是否支援嵌入式 Neo4j?
嵌入式 Neo4j 具有多個面向
SDN 是否直接與嵌入式實例互動?
否。嵌入式資料庫通常由 org.neo4j.graphdb.GraphDatabaseService
的實例表示,且不具備開箱即用的 Bolt 連接器。
然而,SDN 可以與 Neo4j 的測試工具組良好協作,該測試工具組專門設計為真實資料庫的替代品。對 Neo4j 3.5、4.x 和 5.x 測試工具組的支援是透過 驅動程式的 Spring Boot 啟動器 實作。請查看對應的模組 org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure
。
可以使用哪個 Neo4j Java 驅動程式?以及如何使用?
SDN 依賴 Neo4j Java 驅動程式。每個 SDN 版本都使用與發布時最新的可用 Neo4j 相容的 Neo4j Java 驅動程式版本。雖然 Neo4j Java 驅動程式的修補程式版本通常是直接替換,但 SDN 會確保即使是次要版本也是可互換的,因為它會在必要時檢查方法或介面變更的存在與否。
因此,您可以將任何 4.x Neo4j Java 驅動程式與任何 SDN 6.x 版本搭配使用,以及將任何 5.x Neo4j 驅動程式與任何 SDN 7.x 版本搭配使用。
使用 Spring Boot
現今,Spring Boot 部署是 Spring Data 應用程式最常見的部署方式。請使用 Spring Boot 的相依性管理來變更驅動程式版本,如下所示
<properties>
<neo4j-java-driver.version>5.4.0</neo4j-java-driver.version>
</properties>
或
neo4j-java-driver.version = 5.4.0
不使用 Spring Boot
不使用 Spring Boot 時,您只需手動宣告相依性。對於 Maven,我們建議使用 <dependencyManagement />
區段,如下所示
<dependencyManagement> <dependency> <groupId>org.neo4j.driver</groupId> <artifactId>neo4j-java-driver</artifactId> <version>5.4.0</version> </dependency> </dependencyManagement>
Neo4j 4 支援多個資料庫 - 我該如何使用它們?
您可以靜態設定資料庫名稱,或執行您自己的資料庫名稱供應器。請注意,SDN 不會為您建立資料庫。您可以使用 遷移工具 或當然也可以使用簡單的前期腳本來完成此操作。
靜態設定
在您的 Spring Boot 設定中設定要使用的資料庫名稱,如下所示(相同的屬性當然適用於 YML 或環境基礎設定,並套用 Spring Boot 的慣例)
spring.data.neo4j.database = yourDatabase
完成設定後,所有 SDN 儲存庫實例(反應式和命令式)以及 ReactiveNeo4jTemplate
和 Neo4jTemplate
分別產生的所有查詢都將針對 yourDatabase
資料庫執行。
動態設定
依照您的 Spring 應用程式類型,提供類型為 Neo4jDatabaseNameProvider
或 ReactiveDatabaseSelectionProvider
的 Bean。
該 Bean 可以使用 Spring 的安全性上下文來擷取租戶。以下是使用 Spring Security 保護的命令式應用程式的運作範例
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
或更高版本)。
對於命令式和反應式版本,您都需要提供 UserSelectionProvider
或 ReactiveUserSelectionProvider
。相同的實例需要傳遞至 Neo4Client
和 Neo4jTransactionManager
及其反應式變體。
在 無 Boot 命令式 和 反應式 設定中,您只需要提供相關類型的 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,您需要完全自訂用戶端和交易管理器
import org.neo4j.driver.Driver;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.UserSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
public class CustomConfig {
@Bean
public Neo4jClient neo4jClient(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jClient.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
@Bean
public PlatformTransactionManager transactionManager(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jTransactionManager
.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
}
從 Spring Data Neo4j 使用 Neo4j 叢集實例
以下問題適用於 Neo4j AuraDB 以及內部部署 Neo4j 叢集實例。
我是否需要特定的組態,才能使交易與 Neo4j Causal Cluster 無縫協作?
否,您不需要。SDN 在內部使用 Neo4j Causal Cluster 書籤,而無需您端進行任何組態。在同一執行緒或同一反應式串流中彼此相繼的交易將能夠讀取其先前變更的值,如同您預期的一樣。
針對 Neo4j 叢集使用唯讀交易是否重要?
是的,很重要。Neo4j 叢集架構是因果叢集架構,它區分主要伺服器和次要伺服器。主要伺服器可以是單一實例或核心實例。兩者都可以回應讀取和寫入操作。寫入操作從核心實例傳播到叢集內部的讀取副本或更廣泛地說,追隨者。這些追隨者是次要伺服器。次要伺服器不回應寫入操作。
在標準部署情境中,您將在叢集中擁有某些核心實例和許多讀取副本。因此,將操作或查詢標記為唯讀以擴展您的叢集非常重要,這樣領導者永遠不會不堪重負,並且查詢會盡可能地傳播到讀取副本。
Spring Data Neo4j 和底層 Java 驅動程式都不執行 Cypher 剖析,並且這兩個建構區塊預設都假設為寫入操作。做出此決策是為了支援所有開箱即用的操作。如果堆疊中的某些內容預設假設為唯讀,則堆疊最終可能會將寫入查詢傳送到讀取副本,並且執行它們時會失敗。
所有 findById 、findAllById 、findAll 和預先定義的存在性方法預設都標記為唯讀。 |
以下說明一些選項
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 建構實作它的案例,而 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 | 在此,對多個儲存庫的多個呼叫都包裝在一個單一的唯讀交易中。 |
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 和 Proxy 實作的。 |
3 | Neo4jClient 是 SDN 提供的固定公用程式。它無法註解,但它與 Spring 整合。因此,它為您提供了使用純驅動程式所能做的一切,而無需自動映射和交易。它也遵守宣告式交易。 |
我可以擷取最新的書籤或為交易管理器植入種子嗎?
如 書籤管理 中簡要提及,無需針對書籤設定任何內容。然而,擷取 SDN 交易系統從資料庫收到的最新書籤可能很有用。您可以新增類似 BookmarkCapture
的 @Bean
來執行此操作
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();
}
}
為了為交易系統植入種子,需要類似以下的自訂交易管理器
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" 變體,該變體將從相關類別中擷取。
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/Neo4jClient
和 ReactiveNeo4jTransactionManager/ReactiveNeo4jClient
配對,但我們建議僅在您已針對特定資料庫選擇需求設定它們時才這樣做。
我是否需要使用 Neo4j 特定的註解?
否。您可以自由使用以下等效的 Spring Data 註解
SDN 特定註解 | Spring Data 通用註解 | 目的 | 差異 |
---|---|---|---|
|
|
將註解的屬性標記為唯一 ID。 |
特定註解沒有其他功能。 |
|
|
將類別標記為持續性實體。 |
|
我該如何使用指派的 ID?
只需使用不含 @GeneratedValue
的 @Id
,並透過建構子參數或 setter 或 wither 填寫您的 ID 屬性。請參閱這篇 部落格文章,瞭解有關尋找良好 ID 的一些一般性評論。
我該如何使用外部產生的 ID?
我們提供介面 org.springframework.data.neo4j.core.schema.IdGenerator
。以您想要的任何方式實作它,並如下所示設定您的實作
@Node
public class ThingWithGeneratedId {
@Id @GeneratedValue(TestSequenceGenerator.class)
private String theId;
}
如果您將類別名稱傳遞至 @GeneratedValue
,則此類別必須具有無引數預設建構子。但是,您也可以使用字串
@Node
public class ThingWithIdGeneratedByBean {
@Id @GeneratedValue(generatorRef = "idGeneratingBean")
private String theId;
}
這樣一來,idGeneratingBean
便會參照 Spring 上下文中的 Bean。這對於序列產生可能很有用。
ID 的非 final 欄位不需要 Setter。 |
我是否必須為每個領域類別建立儲存庫?
否。請查看 SDN 建構區塊,並尋找 Neo4jTemplate
或 ReactiveNeo4jTemplate
。
這些範本知道您的領域,並提供所有必要的基礎 CRUD 方法,用於擷取、寫入和計數實體。
這是我們使用命令式範本的典型電影範例
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
import org.springframework.data.neo4j.documentation.domain.Roles;
@DataNeo4jTest
public class TemplateExampleTest {
@Test
void shouldSaveAndReadEntities(@Autowired Neo4jTemplate neo4jTemplate) {
MovieEntity movie = new MovieEntity("The Love Bug",
"A movie that follows the adventures of Herbie, Herbie's driver, "
+ "Jim Douglas (Dean Jones), and Jim's love interest, " + "Carole Bennett (Michele Lee)");
Roles roles1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
Roles roles2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
movie.getActorsAndRoles().add(roles1);
movie.getActorsAndRoles().add(roles2);
MovieEntity result = neo4jTemplate.save(movie);
assertThat(result.getActorsAndRoles()).allSatisfy(relationship -> assertThat(relationship.getId()).isNotNull());
Optional<PersonEntity> person = neo4jTemplate.findById("Dean Jones", PersonEntity.class);
assertThat(person).map(PersonEntity::getBorn).hasValue(1931);
assertThat(neo4jTemplate.count(PersonEntity.class)).isEqualTo(2L);
}
}
這是反應式版本,為了簡潔起見省略了設定
import reactor.test.StepVerifier;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
import org.springframework.data.neo4j.documentation.domain.Roles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@DataNeo4jTest
class ReactiveTemplateExampleTest {
@Container private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("org.neo4j.driver.uri", neo4jContainer::getBoltUrl);
registry.add("org.neo4j.driver.authentication.username", () -> "neo4j");
registry.add("org.neo4j.driver.authentication.password", neo4jContainer::getAdminPassword);
}
@Test
void shouldSaveAndReadEntities(@Autowired ReactiveNeo4jTemplate neo4jTemplate) {
MovieEntity movie = new MovieEntity("The Love Bug",
"A movie that follows the adventures of Herbie, Herbie's driver, Jim Douglas (Dean Jones), and Jim's love interest, Carole Bennett (Michele Lee)");
Roles role1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
Roles role2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
movie.getActorsAndRoles().add(role1);
movie.getActorsAndRoles().add(role2);
StepVerifier.create(neo4jTemplate.save(movie)).expectNextCount(1L).verifyComplete();
StepVerifier.create(neo4jTemplate.findById("Dean Jones", PersonEntity.class).map(PersonEntity::getBorn))
.expectNext(1931).verifyComplete();
StepVerifier.create(neo4jTemplate.count(PersonEntity.class)).expectNext(2L).verifyComplete();
}
}
請注意,這兩個範例都使用 Spring Boot 的 @DataNeo4jTest
。
我該如何在儲存庫方法傳回 Page<T>
或 Slice<T>
的情況下使用自訂查詢?
雖然您不必在傳回 Page<T>
或 Slice<T>
的衍生查詢器方法中,除了 Pageable
之外提供任何其他參數,但您必須準備您的自訂查詢來處理可分頁性。頁面和切片 讓您概略瞭解所需內容。
import org.springframework.data.domain.Pageable;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
Page<Person> findByName(String name, Pageable pageable); (1)
@Query(""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit"
)
Slice<Person> findSliceByName(String name, Pageable pageable); (2)
@Query(
value = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit",
countQuery = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN count(n)"
)
Page<Person> findPageByName(String name, Pageable pageable); (3)
}
1 | 為您建立查詢的衍生查詢器方法。它為您處理 Pageable 。您應該使用排序的可分頁性。 |
2 | 此方法使用 @Query 來定義自訂查詢。它傳回 Slice<Person> 。切片不知道總頁數,因此自訂查詢不需要專用的計數查詢。SDN 會通知您,它正在估計下一個切片。Cypher 範本必須找出 $skip 和 $limit Cypher 參數。如果您省略它們,SDN 將發出警告。這可能不符合您的預期。此外,Pageable 應該未排序,並且您應該提供穩定的順序。我們不會使用來自可分頁性的排序資訊。 |
3 | 此方法傳回頁面。頁面知道確切的總頁數。因此,您必須指定額外的計數查詢。第二種方法的所有其他限制都適用。 |
我可以映射具名路徑嗎?
一系列連線的節點和關係在 Neo4j 中稱為「路徑」。Cypher 允許使用識別碼來命名路徑,例如
p = (a)-[*3..5]->(b)
或者如惡名昭彰的電影圖形所示,其中包括以下路徑(在這種情況下,是兩位演員之間的最短路徑之一)
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
RETURN p
看起來像這樣

我們找到 3 個標記為 Vertex
的節點和 2 個標記為 Movie
的節點。兩者都可以使用自訂查詢進行映射。假設 Vertex
和 Movie
以及 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
。Mixins 都允許將條件新增至我們為您建立的完整查詢。因此,您將擁有完全填入的領域以及自訂條件。當然,您的條件必須與我們產生的內容協作。在這裡 尋找 根節點、相關節點等的名稱。 -
透過
CypherdslStatementExecutor
或ReactiveCypherdslStatementExecutor
使用 Cypher-DSL。Cypher-DSL 註定要建立動態查詢。最終,它仍然是 SDN 在底層使用的內容。對應的 Mixin 既適用於儲存庫本身的領域類型,也適用於投影(新增條件的 Mixin 不具備的功能)。
如果您認為可以使用部分動態查詢或完整動態查詢以及投影來解決您的問題,請立即跳回 關於 Spring Data Neo4j Mixin 的章節。
為何現在要談論自訂儲存庫片段?
-
您可能遇到更複雜的情況,其中需要多個動態查詢,但查詢在概念上仍屬於儲存庫,而不是服務層
-
您的自訂查詢傳回圖形狀的結果,該結果不太符合您的領域模型,因此自訂查詢也應伴隨自訂映射
-
您需要與驅動程式互動,即用於不應通過物件映射的批次載入。
假設以下儲存庫 宣告,基本上聚合了一個基礎儲存庫加上 3 個片段
import org.springframework.data.neo4j.repository.Neo4jRepository;
public interface MovieRepository extends Neo4jRepository<MovieEntity, String>,
DomainResults,
NonDomainResults,
LowlevelInteractions {
}
儲存庫擴展的額外介面 (DomainResults
、NonDomainResults
和 LowlevelInteractions
) 是解決上述所有問題的片段。
使用複雜、動態的自訂查詢,但仍傳回網域類型
片段 DomainResults
宣告一個額外的方法 findMoviesAlongShortestPath
interface DomainResults {
@Transactional(readOnly = true)
List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to);
}
此方法使用 @Transactional(readOnly = true)
註解,以表明讀取器可以回覆它。它無法由 SDN 衍生,但需要自訂查詢。此自訂查詢由該介面的一個實作提供。該實作具有相同的名稱,並帶有後綴 Impl
import static org.neo4j.cypherdsl.core.Cypher.anyNode;
import static org.neo4j.cypherdsl.core.Cypher.listWith;
import static org.neo4j.cypherdsl.core.Cypher.name;
import static org.neo4j.cypherdsl.core.Cypher.node;
import static org.neo4j.cypherdsl.core.Cypher.parameter;
import static org.neo4j.cypherdsl.core.Cypher.shortestPath;
import org.neo4j.cypherdsl.core.Cypher;
class DomainResultsImpl implements DomainResults {
private final Neo4jTemplate neo4jTemplate; (1)
DomainResultsImpl(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
}
@Override
public List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to) {
var p1 = node("Person").withProperties("name", parameter("person1"));
var p2 = node("Person").withProperties("name", parameter("person2"));
var shortestPath = shortestPath("p").definedBy(
p1.relationshipBetween(p2).unbounded()
);
var p = shortestPath.getRequiredSymbolicName();
var statement = Cypher.match(shortestPath)
.with(p, listWith(name("n"))
.in(Cypher.nodes(shortestPath))
.where(anyNode().named("n").hasLabels("Movie")).returning().as("mn")
)
.unwind(name("mn")).as("m")
.with(p, name("m"))
.match(node("Person").named("d")
.relationshipTo(anyNode("m"), "DIRECTED").named("r")
)
.returning(p, Cypher.collect(name("r")), Cypher.collect(name("d")))
.build();
Map<String, Object> parameters = new HashMap<>();
parameters.put("person1", from.getName());
parameters.put("person2", to.getName());
return neo4jTemplate.findAll(statement, parameters, MovieEntity.class); (2)
}
}
1 | Neo4jTemplate 在執行時期透過 DomainResultsImpl 的建構子注入。不需要 @Autowired 。 |
2 | Cypher-DSL 用於建構複雜的陳述式(與 路徑映射 中顯示的幾乎相同。)該陳述式可以直接傳遞給範本。 |
範本也具有基於字串的查詢的重載,因此您也可以將查詢寫成字串。這裡重要的重點是
-
範本「知道」您的網域物件並據此映射它們
-
@Query
不是定義自訂查詢的唯一選項 -
它們可以透過多種方式產生
-
@Transactional
註解會被遵循
使用自訂查詢和自訂映射
通常,自訂查詢表示自訂結果。所有這些結果都應該映射為 @Node
嗎?當然不是!很多時候,這些物件代表讀取指令,並非旨在用作寫入指令。SDN 也可能無法或不希望映射 Cypher 可以實現的所有內容。但是,它確實提供了幾個掛鉤來執行您自己的映射:在 Neo4jClient
上。使用 SDN Neo4jClient
而非驅動程式的好處是
-
Neo4jClient
與 Spring 的交易管理整合 -
它具有用於綁定參數的流暢 API
-
它具有流暢的 API,同時公開記錄和 Neo4j 類型系統,以便您可以存取結果中的所有內容以執行映射
宣告片段與之前完全相同
interface NonDomainResults {
class Result { (1)
public final String name;
public final String typeOfRelation;
Result(String name, String typeOfRelation) {
this.name = name;
this.typeOfRelation = typeOfRelation;
}
}
@Transactional(readOnly = true)
Collection<Result> findRelationsToMovie(MovieEntity movie); (2)
}
1 | 這是一個虛構的非網域結果。真實世界的查詢結果可能看起來更複雜。 |
2 | 此片段新增的方法。同樣,該方法使用 Spring 的 @Transactional 註解 |
如果沒有該片段的實作,啟動將會失敗,所以這就是它
class NonDomainResultsImpl implements NonDomainResults {
private final Neo4jClient neo4jClient; (1)
NonDomainResultsImpl(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}
@Override
public Collection<Result> findRelationsToMovie(MovieEntity movie) {
return this.neo4jClient
.query(""
+ "MATCH (people:Person)-[relatedTo]-(:Movie {title: $title}) "
+ "RETURN people.name AS name, "
+ " Type(relatedTo) as typeOfRelation"
) (2)
.bind(movie.getTitle()).to("title") (3)
.fetchAs(Result.class) (4)
.mappedBy((typeSystem, record) -> new Result(record.get("name").asString(),
record.get("typeOfRelation").asString())) (5)
.all(); (6)
}
}
1 | 在這裡我們使用基礎架構提供的 Neo4jClient 。 |
2 | 客戶端僅接受字串,但 Cypher-DSL 仍然可以在渲染為字串時使用 |
3 | 將單個值綁定到具名參數。還有一個重載可以綁定整個參數映射 |
4 | 這是您想要的結果類型 |
5 | 最後,mappedBy 方法,為結果中的每個條目公開一個 Record ,以及驅動程式類型系統(如果需要)。這是您掛鉤自訂映射的 API |
整個查詢在 Spring 交易的上下文中運行,在本例中,是唯讀交易。
低階互動
有時您可能想要從儲存庫執行批量載入,或刪除整個子圖,或以非常特定的方式與 Neo4j Java 驅動程式互動。這也是可能的。以下範例顯示如何操作
interface LowlevelInteractions {
int deleteGraph();
}
class LowlevelInteractionsImpl implements LowlevelInteractions {
private final Driver driver; (1)
LowlevelInteractionsImpl(Driver driver) {
this.driver = driver;
}
@Override
public int deleteGraph() {
try (Session session = driver.session()) {
SummaryCounters counters = session
.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()) (2)
.counters();
return counters.nodesDeleted() + counters.relationshipsDeleted();
}
}
}
1 | 直接使用驅動程式。與所有範例一樣:不需要 @Autowired 魔法。所有片段實際上都可以自行測試。 |
2 | 用例是虛構的。在這裡,我們使用驅動程式管理的交易來刪除整個圖形,並傳回已刪除的節點和關係的數量 |
此互動當然不會在 Spring 交易中運行,因為驅動程式不知道 Spring。
將所有內容放在一起,此測試成功
@Test
void customRepositoryFragmentsShouldWork(
@Autowired PersonRepository people,
@Autowired MovieRepository movies
) {
PersonEntity meg = people.findById("Meg Ryan").get();
PersonEntity kevin = people.findById("Kevin Bacon").get();
List<MovieEntity> moviesBetweenMegAndKevin = movies.
findMoviesAlongShortestPath(meg, kevin);
assertThat(moviesBetweenMegAndKevin).isNotEmpty();
Collection<NonDomainResults.Result> relatedPeople = movies
.findRelationsToMovie(moviesBetweenMegAndKevin.get(0));
assertThat(relatedPeople).isNotEmpty();
assertThat(movies.deleteGraph()).isGreaterThan(0);
assertThat(movies.findAll()).isEmpty();
assertThat(people.findAll()).isEmpty();
}
最後一句話:所有三個介面和實作都會由 Spring Data Neo4j 自動選取。無需進一步設定。此外,相同的整體儲存庫可以使用僅一個額外的片段(定義所有三個方法的介面)和一個實作來建立。然後,該實作將具有所有三個抽象注入(範本、客戶端和驅動程式)。
所有這些當然也適用於反應式儲存庫。它們將與 ReactiveNeo4jTemplate
和 ReactiveNeo4jClient
以及驅動程式提供的反應式會話一起使用。
如果您對所有儲存庫都有重複的方法,則可以換出預設儲存庫實作。
如何使用自訂 Spring Data Neo4j 基礎儲存庫?
基本上與共享 Spring Data Commons 文件中針對 Spring Data JPA 在 自訂基礎儲存庫 中顯示的方式相同。只是在我們的例子中,您將從以下項目擴展
public class MyRepositoryImpl<T, ID> extends SimpleNeo4jRepository<T, ID> {
MyRepositoryImpl(
Neo4jOperations neo4jOperations,
Neo4jEntityInformation<T, ID> entityInformation
) {
super(neo4jOperations, entityInformation); (1)
}
@Override
public List<T> findAll() {
throw new UnsupportedOperationException("This implementation does not support `findAll`");
}
}
1 | 此簽名是基底類別所必需的。取得 Neo4jOperations (Neo4jTemplate 的實際規範) 和實體資訊,並根據需要將它們儲存在屬性中。 |
在此範例中,我們禁止使用 findAll
方法。您可以新增方法來接收提取深度,並根據該深度執行自訂查詢。執行此操作的一種方法在 DomainResults 片段 中顯示。
若要為所有宣告的儲存庫啟用此基礎儲存庫,請使用以下內容啟用 Neo4j 儲存庫:@EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class)
。
如何稽核實體?
支援所有 Spring Data 註解。那些是
-
org.springframework.data.annotation.CreatedBy
-
org.springframework.data.annotation.CreatedDate
-
org.springframework.data.annotation.LastModifiedBy
-
org.springframework.data.annotation.LastModifiedDate
稽核 讓您大致了解如何在 Spring Data Commons 的更大範圍內使用稽核。以下清單呈現 Spring Data Neo4j 提供的每個設定選項
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.domain.AuditorAware;
@Configuration
@EnableNeo4jAuditing(
modifyOnCreate = false, (1)
auditorAwareRef = "auditorProvider", (2)
dateTimeProviderRef = "fixedDateTimeProvider" (3)
)
class AuditingConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of("A user");
}
@Bean
public DateTimeProvider fixedDateTimeProvider() {
return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE);
}
}
1 | 如果您希望在建立期間也寫入修改資料,請設定為 true |
2 | 使用此屬性指定提供稽核員(即使用者名稱)的 bean 的名稱 |
3 | 使用此屬性指定提供當前日期的 bean 的名稱。在本例中,使用固定日期,因為上述設定是我們測試的一部分 |
反應式版本基本上相同,除了稽核員感知 bean 的類型為 ReactiveAuditorAware
,因此稽核員的檢索是反應式流程的一部分。
除了這些稽核機制之外,您還可以將任意數量的實作 BeforeBindCallback<T>
或 ReactiveBeforeBindCallback<T>
的 bean 新增到上下文中。這些 bean 將被 Spring Data Neo4j 選取,並在實體持久化之前按順序(如果它們實作 Ordered
或使用 @Order
註解)呼叫。
它們可以修改實體或傳回全新的實體。以下範例將一個回呼新增到上下文中,該回呼在實體持久化之前變更一個屬性
import java.util.UUID;
import java.util.stream.StreamSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.mapping.callback.AfterConvertCallback;
import org.springframework.data.neo4j.core.mapping.callback.BeforeBindCallback;
@Configuration
class CallbacksConfig {
@Bean
BeforeBindCallback<ThingWithAssignedId> nameChanger() {
return entity -> {
ThingWithAssignedId updatedThing = new ThingWithAssignedId(
entity.getTheId(), entity.getName() + " (Edited)");
return updatedThing;
};
}
@Bean
AfterConvertCallback<ThingWithAssignedId> randomValueAssigner() {
return (entity, definition, source) -> {
entity.setRandomValue(UUID.randomUUID().toString());
return entity;
};
}
}
無需額外設定。
如何使用「範例查詢」?
「範例查詢」是 SDN 中的一項新功能。您實例化一個實體或使用現有的實體。使用此實例,您可以建立 org.springframework.data.domain.Example
。如果您的儲存庫擴展了 org.springframework.data.neo4j.repository.Neo4jRepository
或 org.springframework.data.neo4j.repository.ReactiveNeo4jRepository
,您可以立即使用可用的 findBy
方法來接收範例,如 findByExample 中所示。
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
運算,從而將 =
變成 <>
。支援所有純量資料類型和所有字串運算子
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>7.4.0</version>
</dependency>
Gradle 設定的座標是相同的。
若要選取不同的資料庫 - 無論是靜態或動態 - 您可以新增類型為 DatabaseSelectionProvider
的 Bean,如 Neo4j 4 支援多個資料庫 - 我該如何使用它們? 中所述。對於反應式場景,我們提供 ReactiveDatabaseSelectionProvider
。
在沒有 Spring Boot 的 Spring 上下文中使用 Spring Data Neo4j
我們提供兩個抽象設定類別,以協助您引入必要的 bean:org.springframework.data.neo4j.config.AbstractNeo4jConfig
用於命令式資料庫存取,org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig
用於反應式版本。它們旨在分別與 @EnableNeo4jRepositories
和 @EnableReactiveNeo4jRepositories
一起使用。請參閱 為命令式資料庫存取啟用 Spring Data Neo4j 基礎架構 和 為反應式資料庫存取啟用 Spring Data Neo4j 基礎架構 以取得範例用法。這兩個類別都要求您覆寫 driver()
,您應該在其中建立驅動程式。
若要取得 Neo4j 客戶端 的命令式版本、範本和對命令式儲存庫的支援,請使用類似於此處顯示的內容
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 相關的儲存庫
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig;
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableReactiveNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractReactiveNeo4jConfig {
@Bean
@Override
public Driver driver() {
return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
@Override
protected Collection<String> getMappingBasePackages() {
return Collections.singletonList(Person.class.getPackage().getName());
}
}
在 CDI 2.0 環境中使用 Spring Data Neo4j
為了您的方便,我們提供了帶有 Neo4jCdiExtension
的 CDI 擴充功能。當在相容的 CDI 2.0 容器中執行時,它將透過 Java 的服務載入器 SPI 自動註冊和載入。
您必須引入到應用程式中的唯一內容是產生 Neo4j Java 驅動程式的註解類型
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
public class Neo4jConfig {
@Produces @ApplicationScoped
public Driver driver() { (1)
return GraphDatabase
.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
public void close(@Disposes Driver driver) {
driver.close();
}
@Produces @Singleton
public DatabaseSelectionProvider getDatabaseSelectionProvider() { (2)
return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
}
}
1 | 與 為命令式資料庫存取啟用 Spring Data Neo4j 基礎架構 中的純 Spring 相同,但使用對應的 CDI 基礎架構進行註解。 |
2 | 這是可選的。但是,如果您執行自訂資料庫選取提供者,則不得限定此 bean。 |
如果您在 SE 容器中執行 - 例如 Weld 提供的容器,您可以像這樣啟用擴充功能
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();
}
}
}