常见问题
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 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 的依赖管理来更改驱动程序版本,如下所示
<properties>
<neo4j-java-driver.version>5.4.0</neo4j-java-driver.version>
</properties>
Or
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 因果集群无缝工作吗?
不需要。SDN 内部使用 Neo4j 因果集群书签,无需您进行任何配置。在同一线程或同一响应式流中彼此跟随的事务将能够按预期读取其先前更改的值。
对于 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 构造实现它,这当然是一个写操作。 |
| 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 | 在这里,对多个存储库的多个调用被包装在一个单独的只读事务中。 |
TransactionTemplateimport 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 来实现此目的
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?
只需使用 @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。 |
我必须为每个领域类创建存储库吗?
不需要。请查看 SDN 构建块 并找到 Neo4jTemplate 或 ReactiveNeo4jTemplate。
这些模板了解您的领域并提供所有必要的基本 CRUD 方法,用于检索、写入和计数实体。
这是我们使用命令式模板的典型电影示例
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);
}
}
这是响应式版本,为简洁起见省略了设置
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
它看起来像这样
我们发现 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。这两个 混合器 都允许向我们为您创建的完整查询添加条件。因此,您将拥有完全填充的域以及自定义条件。当然,您的条件必须与我们生成的内容兼容。在此处 查找 根节点、相关节点等的名称。 -
通过
CypherdslStatementExecutor或ReactiveCypherdslStatementExecutor使用 Cypher-DSL。Cypher-DSL 注定要创建动态查询。最终,SDN 无论如何都在底层使用它。相应的混合器既适用于存储库本身的域类型,也适用于投影(条件混合器不支持此功能)。
如果您认为可以通过部分动态查询或完全动态查询与投影结合来解决您的问题,请立即跳回到 关于 Spring Data Neo4j Mixins 的章节。
为什么现在要谈论自定义存储库片段?
-
您可能遇到更复杂的情况,需要多个动态查询,但这些查询在概念上仍属于存储库而非服务层
-
您的自定义查询返回的图结构结果与您的域模型不太匹配,因此自定义查询也应伴随自定义映射
-
您需要与驱动程序交互,例如,进行不应通过对象映射的批量加载。
假设以下存储库声明,它基本上聚合了一个基本存储库和 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 {
}
存储库扩展的附加接口(DomainResults、NonDomainResults 和 LowlevelInteractions)是解决上述所有问题的片段。
使用复杂的动态自定义查询但仍返回域类型
片段 DomainResults 声明了一个额外的 findMoviesAlongShortestPath 方法
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
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 进行注解 |
如果没有该片段的实现,启动将失败,所以这里是
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 自动获取。无需进一步配置。此外,相同的整体存储库可以只用一个附加片段(定义所有三个方法的接口)和一个实现来创建。然后,该实现将注入所有三个抽象(模板、客户端和驱动程序)。
所有这些当然也适用于响应式存储库。它们将与 ReactiveNeo4jTemplate 和 ReactiveNeo4jClient 以及驱动程序提供的响应式会话一起工作。
如果您所有存储库都有重复方法,则可以替换默认存储库实现。
如何使用自定义 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 提供的所有配置选项
@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.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>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 客户端、模板和对命令式存储库的支持,请使用此处所示的类似方法
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 相关存储库
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 驱动程序
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();
}
}
}