常見問題解答
SDN 與 Neo4j-OGM 有何關係?
Neo4j-OGM 是一個物件圖對映庫,主要用於 Spring Data Neo4j 的早期版本,作為其後端,負責將節點和關係對映到領域物件這一繁重工作。當前的 SDN **不需要**也**不支援** Neo4j-OGM。SDN 完全使用 Spring Data 的對映上下文來掃描類和構建元模型。
雖然這使得 SDN 與 Spring 生態系統緊密相連,但它有幾個優點,其中包括更小的 CPU 和記憶體佔用,特別是 Spring 對映上下文的所有特性。
SDN 是否支援嵌入式 Neo4j?
嵌入式 Neo4j 有多個方面:
SDN 是否直接與嵌入式例項互動?
不支援。嵌入式資料庫通常由 org.neo4j.graphdb.GraphDatabaseService
的例項表示,預設沒有 Bolt 聯結器。
然而,SDN 可以很好地與 Neo4j 的測試工具(test harness)配合使用,該測試工具專門設計用於替代真實的資料庫。透過驅動程式的 Spring Boot Starter 實現了對 Neo4j 3.5, 4.x 和 5.x 測試工具的支援。請檢視相應的模組 org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure
。
可以使用哪些 Neo4j Java Driver,以及如何使用?
SDN 依賴於 Neo4j Java Driver。每個 SDN 版本都使用與其釋出時最新的 Neo4j 相容的 Neo4j Java Driver 版本。雖然 Neo4j Java Driver 的補丁版本通常可以直接替換,但 SDN 確保即使是次要版本也可以互換,因為它在必要時會檢查方法或介面是否存在或發生變化。
因此,您可以使用任何 4.x 版本的 Neo4j Java Driver 與任何 6.x 版本的 SDN,以及任何 5.x 版本的 Neo4j Driver 與任何 7.x 版本的 SDN。
使用 Spring Boot
如今,Spring Boot 部署是基於 Spring Data 的應用程式最可能的部署方式。請使用 Spring Boot 的依賴管理來更改 Driver 版本,如下所示:
<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 Repository 例項(無論是響應式還是命令式)以及 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 Driver 連線的是伺服器,而不是伺服器內的特定資料庫。Spring Boot 能夠在不依賴 Spring Data Neo4j 的情況下配置 Driver,並且由於要使用的資料庫資訊與 Spring Data Neo4j 繫結,因此內建的健康指標無法獲取此資訊。
這在許多部署場景中很可能不是問題。但是,如果配置的資料庫使用者至少沒有對預設資料庫的訪問許可權,健康檢查將會失敗。
這可以通過了解資料庫選擇的自定義 Neo4j 健康貢獻者來緩解。
命令式變體
import java.util.Optional;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Result;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.summary.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.util.StringUtils;
public class DatabaseSelectionAwareNeo4jHealthIndicator extends AbstractHealthIndicator {
private final Driver driver;
private final DatabaseSelectionProvider databaseSelectionProvider;
public DatabaseSelectionAwareNeo4jHealthIndicator(
Driver driver, DatabaseSelectionProvider databaseSelectionProvider
) {
this.driver = driver;
this.databaseSelectionProvider = databaseSelectionProvider;
}
@Override
protected void doHealthCheck(Health.Builder builder) {
try {
SessionConfig sessionConfig = Optional
.ofNullable(databaseSelectionProvider.getDatabaseSelection())
.filter(databaseSelection -> databaseSelection != DatabaseSelection.undecided())
.map(DatabaseSelection::getValue)
.map(v -> SessionConfig.builder().withDatabase(v).build())
.orElseGet(SessionConfig::defaultConfig);
class Tuple {
String edition;
ResultSummary resultSummary;
Tuple(String edition, ResultSummary resultSummary) {
this.edition = edition;
this.resultSummary = resultSummary;
}
}
String query =
"CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
Tuple health = driver.session(sessionConfig)
.writeTransaction(tx -> {
Result result = tx.run(query);
String edition = result.single().get("edition").asString();
return new Tuple(edition, result.consume());
});
addHealthDetails(builder, health.edition, health.resultSummary);
} catch (Exception ex) {
builder.down().withException(ex);
}
}
static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
ServerInfo serverInfo = resultSummary.server();
builder.up()
.withDetail(
"server", serverInfo.version() + "@" + serverInfo.address())
.withDetail("edition", edition);
DatabaseInfo databaseInfo = resultSummary.database();
if (StringUtils.hasText(databaseInfo.name())) {
builder.withDetail("database", databaseInfo.name());
}
}
}
這使用可用的資料庫選擇來執行與 Boot 相同的查詢,以檢查連線是否健康。使用以下配置來應用它:
import java.util.Map;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {
@Bean (1)
DatabaseSelectionAwareNeo4jHealthIndicator databaseSelectionAwareNeo4jHealthIndicator(
Driver driver, DatabaseSelectionProvider databaseSelectionProvider
) {
return new DatabaseSelectionAwareNeo4jHealthIndicator(driver, databaseSelectionProvider);
}
@Bean (2)
HealthContributor neo4jHealthIndicator(
Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators) {
return CompositeHealthContributor.fromMap(customNeo4jHealthIndicators);
}
@Bean (3)
InitializingBean healthContributorRegistryCleaner(
HealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators
) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
}
1 | 如果您有多個 Driver 和資料庫選擇提供者,則需要為每種組合建立一個指標。 |
2 | 這確保所有這些指標都分組在 Neo4j 下,替換預設的 Neo4j 健康指標。 |
3 | 這阻止單個貢獻者直接出現在健康端點中。 |
響應式變體
響應式變體基本相同,使用響應式型別和相應的響應式基礎設施類。
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import org.neo4j.driver.Driver;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.reactivestreams.RxResult;
import org.neo4j.driver.reactivestreams.RxSession;
import org.neo4j.driver.summary.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import org.reactivestreams.Publisher;
import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.util.StringUtils;
public final class DatabaseSelectionAwareNeo4jReactiveHealthIndicator
extends AbstractReactiveHealthIndicator {
private final Driver driver;
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
public DatabaseSelectionAwareNeo4jReactiveHealthIndicator(
Driver driver,
ReactiveDatabaseSelectionProvider databaseSelectionProvider
) {
this.driver = driver;
this.databaseSelectionProvider = databaseSelectionProvider;
}
@Override
protected Mono<Health> doHealthCheck(Health.Builder builder) {
String query =
"CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
return databaseSelectionProvider.getDatabaseSelection()
.map(databaseSelection -> databaseSelection == DatabaseSelection.undecided() ?
SessionConfig.defaultConfig() :
SessionConfig.builder().withDatabase(databaseSelection.getValue()).build()
)
.flatMap(sessionConfig ->
Mono.usingWhen(
Mono.fromSupplier(() -> driver.rxSession(sessionConfig)),
s -> {
Publisher<Tuple2<String, ResultSummary>> f = s.readTransaction(tx -> {
RxResult result = tx.run(query);
return Mono.from(result.records())
.map((record) -> record.get("edition").asString())
.zipWhen((edition) -> Mono.from(result.consume()));
});
return Mono.fromDirect(f);
},
RxSession::close
)
).map((result) -> {
addHealthDetails(builder, result.getT1(), result.getT2());
return builder.build();
});
}
static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
ServerInfo serverInfo = resultSummary.server();
builder.up()
.withDetail(
"server", serverInfo.version() + "@" + serverInfo.address())
.withDetail("edition", edition);
DatabaseInfo databaseInfo = resultSummary.database();
if (StringUtils.hasText(databaseInfo.name())) {
builder.withDetail("database", databaseInfo.name());
}
}
}
當然,還有配置的響應式變體。它需要兩個不同的登錄檔清理器,因為 Spring Boot 也會包裝現有的響應式指標以用於非響應式 actuator 端點。
import java.util.Map;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
import org.springframework.boot.actuate.health.HealthContributorNameFactory;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.ReactiveHealthContributor;
import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {
@Bean
ReactiveHealthContributor neo4jHealthIndicator(
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return CompositeReactiveHealthContributor.fromMap(customNeo4jHealthIndicators);
}
@Bean
InitializingBean healthContributorRegistryCleaner(HealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
@Bean
InitializingBean reactiveHealthContributorRegistryCleaner(
ReactiveHealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
}
Neo4j 4.4+ 支援模擬不同使用者 - 如何使用它們?
使用者模擬在大型多租戶設定中特別有趣,其中一個物理連線(或技術)使用者可以模擬許多租戶。根據您的設定,這將大大減少所需的物理 Driver 例項數量。
該特性要求伺服器端使用 Neo4j Enterprise 4.4+,客戶端使用 4.4+ Driver (org.neo4j.driver:neo4j-java-driver:4.4.0
或更高版本)。
對於命令式和響應式版本,您需要分別提供一個 UserSelectionProvider
或一個 ReactiveUserSelectionProvider
。同一個例項需要傳遞給 Neo4Client
和 Neo4jTransactionManager
或其相應的響應式變體。
在 不使用 Boot 的命令式 和 不使用 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,您需要完全自定義 Client 和事務管理器。
import org.neo4j.driver.Driver;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.UserSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
public class CustomConfig {
@Bean
public Neo4jClient neo4jClient(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jClient.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
@Bean
public PlatformTransactionManager transactionManager(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jTransactionManager
.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
}
從 Spring Data Neo4j 使用 Neo4j 叢集例項
以下問題適用於 Neo4j AuraDB 和本地部署的 Neo4j 叢集例項。
我需要特定的配置才能讓事務與 Neo4j Causal Cluster 無縫協作嗎?
不需要。SDN 在內部使用 Neo4j Causal Cluster 書籤,無需您進行任何配置。同一執行緒或同一響應式流中緊隨其後的事務將能夠讀取其先前更改的值,正如您所期望的那樣。
對 Neo4j 叢集使用只讀事務重要嗎?
是的,很重要。Neo4j 叢集架構是一種因果叢集架構,它區分主伺服器和從伺服器。主伺服器可以是單個例項或核心例項。它們都可以響應讀寫操作。寫操作從核心例項傳播到叢集內的讀副本或更一般的關注者。這些關注者是從伺服器。從伺服器不響應寫操作。
在標準部署場景中,您將有一些核心例項和叢集內的許多讀副本。因此,將操作或查詢標記為只讀非常重要,這樣可以以一種領導者永不超載且查詢儘可能傳播到讀副本的方式來擴充套件您的叢集。
Spring Data Neo4j 和底層的 Java Driver 都不進行 Cypher 解析,並且這兩個構建塊預設都假定是寫操作。做出此決定是為了開箱即用支援所有操作。如果在棧中的某個地方預設假定為只讀,則棧可能會將寫查詢傳送到讀副本並執行失敗。
所有 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 構造來實現它的情況,這當然是一個寫操作。 |
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 | 在這裡,對多個 Repository 的幾次呼叫被包裝在一個單一的只讀事務中。 |
TransactionTemplate
import java.util.Collection;
import org.neo4j.driver.types.Node;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
public class PersonService {
private final TransactionTemplate readOnlyTx;
private final Neo4jClient neo4jClient;
public PersonService(PlatformTransactionManager transactionManager, Neo4jClient neo4jClient) {
this.readOnlyTx = new TransactionTemplate(transactionManager, (1)
new TransactionDefinition() {
@Override public boolean isReadOnly() {
return true;
}
}
);
this.neo4jClient = neo4jClient;
}
void internalOperation() { (2)
Collection<Node> nodes = this.readOnlyTx.execute(state -> {
return neo4jClient.query("MATCH (n) RETURN n").fetchAs(Node.class) (3)
.mappedBy((types, record) -> record.get(0).asNode())
.all();
});
}
}
1 | 建立具有您所需特性的 TransactionTemplate 例項。當然,這也可以是一個全域性 Bean。 |
2 | 使用事務模板的首要原因:宣告式事務在其本質上使用 Aspects 和代理實現,因此無法在包私有或私有方法中工作,也無法在內部方法呼叫(想象此 Service 中的另一個方法呼叫 internalOperation )中工作。 |
3 | Neo4jClient 是 SDN 提供的固定工具類。它不能被註解,但它與 Spring 整合。因此,它為您提供了使用純粹的 Driver 可以做的一切,並且無需自動對映,同時支援事務。它也遵循宣告式事務。 |
我可以檢索最新的書籤或為事務管理器“播種”嗎?
正如在書籤管理中簡要提到的,無需配置任何與書籤相關的內容。然而,檢索 SDN 事務系統從資料庫接收到的最新書籤可能很有用。您可以新增一個類似 BookmarkCapture
的 @Bean
來實現此目的:
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 | 此 Supplier 可以是持有您希望引入系統中的最新書籤的任何東西。 |
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?
只需使用 @Id
而不使用 @GeneratedValue
,並透過建構函式引數、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。這對於序列生成可能很有用。
非 final 欄位的 ID 不需要 Setter 方法。 |
我必須為每個領域類建立一個 Repository 嗎?
不需要。請檢視 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>
的 Repository 方法?
雖然對於返回 Page<T>
或 Slice<T>
的派生查詢方法,您除了 Pageable
引數外無需提供其他任何內容,但您必須準備自定義查詢以處理 Pageable。頁面和切片 概述了所需的內容。
import org.springframework.data.domain.Pageable;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
Page<Person> findByName(String name, Pageable pageable); (1)
@Query(""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit"
)
Slice<Person> findSliceByName(String name, Pageable pageable); (2)
@Query(
value = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit",
countQuery = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN count(n)"
)
Page<Person> findPageByName(String name, Pageable pageable); (3)
}
1 | 一個派生查詢方法,它為您建立查詢。它為您處理 Pageable。您應該使用已排序的 Pageable。 |
2 | 此方法使用 @Query 定義自定義查詢。它返回一個 Slice<Person> 。Slice 不知道總頁數,因此自定義查詢不需要單獨的計數查詢。SDN 會通知您它正在估算下一個切片。Cypher 模板必須包含 $skip 和 $limit 這兩個 Cypher 引數。如果省略它們,SDN 將發出警告。結果可能與您的預期不符。此外,Pageable 應該未排序,並且您應該提供一個穩定的順序。我們不會使用來自 Pageable 的排序資訊。 |
3 | 此方法返回一個 Page。Page 知道總頁數的精確數量。因此,您必須指定一個額外的計數查詢。第二種方法的所有其他限制也適用。 |
我可以對映命名路徑嗎?
在 Neo4j 中,一系列連線的節點和關係被稱為“路徑”。Cypher 允許使用識別符號為路徑命名,例如:
p = (a)-[*3..5]->(b)
或者像臭名昭著的電影圖一樣,包含以下路徑(在本例中,這是兩個演員之間的最短路徑之一):
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
RETURN p
看起來像這樣:

我們找到 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
這樣的關係型別,並且它們也在領域模型中存在,則這些關係將根據路徑進行填充。
當您使用基於路徑查詢水化(hydrated)的節點來儲存資料時,請特別注意。如果不是所有的關係都被水化,資料將會丟失。 |
反之亦然。相同的查詢可以用於 Movie
實體。此時它將只填充電影。以下列表展示瞭如何做到這一點,以及如何用路徑上未找到的額外資料豐富查詢。這些資料用於正確填充缺失的關係(在本例中,是所有的演員):
interface MovieRepository extends Neo4jRepository<Movie, String> {
@Query(""
+ "MATCH p=shortestPath(\n"
+ "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
+ "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
+ "UNWIND x AS m\n"
+ "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
+ "RETURN p, collect(r), collect(d)"
)
List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
}
查詢返回路徑以及所有收集到的關係和相關節點,以便電影實體被完全水化。
路徑對映適用於單個路徑以及多條路徑記錄(由 allShortestPath
函式返回)。
命名路徑可以有效地用於填充和返回不僅僅是根節點,請參閱 附錄/自定義查詢.adoc#自定義查詢.路徑。 |
@Query
是使用自定義查詢的唯一方法嗎?
不是,@Query
**不是**執行自定義查詢的唯一方法。當您的自定義查詢完全填充您的領域模型時,這個註解很方便。請記住,SDN 假定您對映的領域模型是真實的。這意味著如果您透過 @Query
使用一個只部分填充模型的自定義查詢,您就有危險使用同一個物件將資料寫回去,這最終會擦除或覆蓋您在查詢中未考慮的資料。
因此,請在結果形狀與您的領域模型一致,或者您確信您不會使用部分對映的模型進行寫入操作的所有情況下,使用 Repository 和帶有 @Query
的宣告式方法。
還有哪些替代方案?
-
投影(Projections)可能已經足夠塑造您對圖的“檢視”:它們可以用來以明確的方式定義屬性和相關實體的抓取深度:透過對它們進行建模。
-
如果您的目標是隻讓查詢的條件變得動態,那麼請檢視
QuerydslPredicateExecutor
,特別是我們自己的變體CypherdslConditionExecutor
。這兩個Mixin都允許將條件新增到我們為您建立的完整查詢中。因此,您將擁有完全填充的領域模型以及自定義條件。當然,您的條件必須與我們生成的內容相容。在此處查詢根節點、相關節點等的名稱。 -
透過
CypherdslStatementExecutor
或ReactiveCypherdslStatementExecutor
使用 Cypher-DSL。Cypher-DSL 註定是用於建立動態查詢的。最終,它也是 SDN 在底層使用的東西。相應的 Mixin 既可以與 Repository 自身的領域型別一起工作,也可以與投影一起工作(這是新增條件的 Mixin 不具備的功能)。
如果您認為可以透過部分動態查詢或完全動態查詢以及投影來解決您的問題,那麼現在請跳回到 關於 Spring Data Neo4j Mixin 的章節。
否則,請閱讀以下兩方面的內容:自定義 Repository 片段 和 我們在 SDN 中提供的抽象級別。
為什麼現在要談論自定義 Repository 片段?
-
您可能遇到更復雜的情況,需要不止一個動態查詢,但這些查詢在概念上仍屬於 Repository 而非 Service 層
-
您的自定義查詢返回一個圖形狀的結果,這與您的領域模型不太匹配,因此自定義查詢應伴隨自定義對映
-
您需要與 Driver 互動,例如用於不應透過物件對映進行的批次載入。
假設以下 Repository 宣告,它基本上聚合了一個基本 Repository 外加 3 個片段:
import org.springframework.data.neo4j.repository.Neo4jRepository;
public interface MovieRepository extends Neo4jRepository<MovieEntity, String>,
DomainResults,
NonDomainResults,
LowlevelInteractions {
}
Repository 擴充套件的附加介面(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 而非 Driver 的好處是:
-
Neo4jClient
與 Spring 的事務管理整合 -
它提供了用於繫結引數的流暢 API
-
它提供了一個流暢的 API,暴露了記錄和 Neo4j 型別系統,這樣您就可以訪問結果中的所有內容來執行對映。
宣告片段與之前完全相同:
interface NonDomainResults {
class Result { (1)
public final String name;
public final String typeOfRelation;
Result(String name, String typeOfRelation) {
this.name = name;
this.typeOfRelation = typeOfRelation;
}
}
@Transactional(readOnly = true)
Collection<Result> findRelationsToMovie(MovieEntity movie); (2)
}
1 | 這是一個虛構的非領域結果。實際的查詢結果可能會更復雜。 |
2 | 此片段新增的方法。再次,該方法用 Spring 的 @Transactional 註解。 |
如果缺少該片段的實現,啟動將會失敗,因此實現如下:
class NonDomainResultsImpl implements NonDomainResults {
private final Neo4jClient neo4jClient; (1)
NonDomainResultsImpl(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}
@Override
public Collection<Result> findRelationsToMovie(MovieEntity movie) {
return this.neo4jClient
.query(""
+ "MATCH (people:Person)-[relatedTo]-(:Movie {title: $title}) "
+ "RETURN people.name AS name, "
+ " Type(relatedTo) as typeOfRelation"
) (2)
.bind(movie.getTitle()).to("title") (3)
.fetchAs(Result.class) (4)
.mappedBy((typeSystem, record) -> new Result(record.get("name").asString(),
record.get("typeOfRelation").asString())) (5)
.all(); (6)
}
}
1 | 這裡我們使用由基礎設施提供的 Neo4jClient 。 |
2 | Client 只接受字串,但在渲染為字串時仍然可以使用 Cypher-DSL。 |
3 | 將單個值繫結到命名引數。還有一個過載可以繫結整個引數 Map。 |
4 | 這是您想要的結果型別 |
5 | 最後,是 mappedBy 方法,它暴露結果中的每一條記錄以及(如果需要)Driver 的型別系統。這是您鉤入進行自定義對映的 API。 |
整個查詢在 Spring 事務的上下文中執行,在本例中,是隻讀事務。
低層互動
有時您可能希望從 Repository 執行批次載入或刪除整個子圖,或者以非常特定的方式與 Neo4j Java-Driver 互動。這也是可能的。以下示例展示瞭如何操作:
interface LowlevelInteractions {
int deleteGraph();
}
class LowlevelInteractionsImpl implements LowlevelInteractions {
private final Driver driver; (1)
LowlevelInteractionsImpl(Driver driver) {
this.driver = driver;
}
@Override
public int deleteGraph() {
try (Session session = driver.session()) {
SummaryCounters counters = session
.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()) (2)
.counters();
return counters.nodesDeleted() + counters.relationshipsDeleted();
}
}
}
1 | 直接使用驅動程式。與所有示例一樣:無需 @Autowired 的魔力。所有片段都可以獨立進行測試。 |
2 | 用例是虛構的。這裡我們使用驅動程式管理的事務,刪除整個圖並返回刪除的節點和關係的數量。 |
當然,這種互動不會在 Spring 事務中執行,因為驅動程式不知道 Spring。
將所有這些放在一起,這個測試成功了
@Test
void customRepositoryFragmentsShouldWork(
@Autowired PersonRepository people,
@Autowired MovieRepository movies
) {
PersonEntity meg = people.findById("Meg Ryan").get();
PersonEntity kevin = people.findById("Kevin Bacon").get();
List<MovieEntity> moviesBetweenMegAndKevin = movies.
findMoviesAlongShortestPath(meg, kevin);
assertThat(moviesBetweenMegAndKevin).isNotEmpty();
Collection<NonDomainResults.Result> relatedPeople = movies
.findRelationsToMovie(moviesBetweenMegAndKevin.get(0));
assertThat(relatedPeople).isNotEmpty();
assertThat(movies.deleteGraph()).isGreaterThan(0);
assertThat(movies.findAll()).isEmpty();
assertThat(people.findAll()).isEmpty();
}
最後補充一點:Spring Data Neo4j 會自動拾取所有這三個介面和實現。無需進一步配置。此外,只需增加一個額外的片段(定義所有三個方法的介面)和一個實現,就可以建立相同的整體倉庫。該實現將注入所有這三個抽象(template、client 和 driver)。
當然,所有這些也適用於響應式倉庫。它們將與 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。Spring Data Neo4j 將在實體持久化之前立即拾取這些 bean 並按順序呼叫它們(如果它們實現了 Ordered
或用 @Order
註解)。
它們可以修改實體或返回一個全新的實體。以下示例向上下文中添加了一個回撥,該回調在實體持久化之前更改了一個屬性
import java.util.UUID;
import java.util.stream.StreamSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.mapping.callback.AfterConvertCallback;
import org.springframework.data.neo4j.core.mapping.callback.BeforeBindCallback;
@Configuration
class CallbacksConfig {
@Bean
BeforeBindCallback<ThingWithAssignedId> nameChanger() {
return entity -> {
ThingWithAssignedId updatedThing = new ThingWithAssignedId(
entity.getTheId(), entity.getName() + " (Edited)");
return updatedThing;
};
}
@Bean
AfterConvertCallback<ThingWithAssignedId> randomValueAssigner() {
return (entity, definition, source) -> {
entity.setRandomValue(UUID.randomUUID().toString());
return entity;
};
}
}
無需額外配置。
如何使用“按示例查詢”?
“按示例查詢”是 SDN 中的一個新特性。您可以例項化一個實體或使用現有實體。使用此例項,您可以建立一個 org.springframework.data.domain.Example
。如果您的倉庫繼承自 org.springframework.data.neo4j.repository.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 Data Neo4j 需要 Spring Boot 嗎?
不,不需要。雖然 Spring Boot 透過自動配置許多 Spring 方面消除了大量手動操作,並且是設定新 Spring 專案的推薦方法,但您不必非要使用它。
上述解決方案需要以下依賴項
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>7.4.5</version>
</dependency>
Gradle 配置的座標是相同的。
要選擇不同的資料庫(靜態或動態),您可以新增一個 DatabaseSelectionProvider
型別的 Bean,如 Neo4j 4 支援多資料庫 - 如何使用它們? 中所解釋的。對於響應式場景,我們提供了 ReactiveDatabaseSelectionProvider
。
在沒有 Spring Boot 的 Spring 上下文中使用 Spring Data Neo4j
我們提供了兩個抽象配置類來幫助您引入必要的 Bean:org.springframework.data.neo4j.config.AbstractNeo4jConfig
用於命令式資料庫訪問,org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig
用於響應式版本。它們分別與 @EnableNeo4jRepositories
和 @EnableReactiveNeo4jRepositories
一起使用。使用示例請參見 為命令式資料庫訪問啟用 Spring Data Neo4j 基礎設施 和 為響應式資料庫訪問啟用 Spring Data Neo4j 基礎設施。這兩個類都要求您重寫 driver()
方法,您應該在該方法中建立驅動程式。
要獲取命令式版本的 Neo4j client、模板以及對命令式倉庫的支援,請使用此處所示的類似方法
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 client 和模板,啟用響應式事務管理並發現與 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 Driver 的帶註解型別
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 提供的容器),您可以這樣啟用擴充套件
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();
}
}
}