常见问题解答
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 的响应式编程,包括响应式事务
-
全面支持 Query By Example
-
全面支持完全不可变的实体
-
支持派生查询方法的所有修饰符和变体,包括空间查询
SDN 是否支持通过 HTTP 连接到 Neo4j?
否。
SDN 是否支持嵌入式 Neo4j?
嵌入式 Neo4j 具有多个方面:
SDN 是否为您的应用程序提供嵌入式实例?
不。
SDN 是否直接与嵌入式实例交互?
不。一个嵌入式数据库通常由 org.neo4j.graphdb.GraphDatabaseService
的实例表示,并且默认情况下没有 Bolt 连接器。
然而,SDN 可以与 Neo4j 的测试工具(test harness)很好地协同工作,该测试工具专门设计为真实数据库的直接替代品。对 Neo4j 3.5、4.x 和 5.x 测试工具的支持通过 the Spring Boot starter for the driver 实现。请参考相应的模块 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 Data 的应用程序最有可能采用 Spring Boot 进行部署。请使用 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 不会为你创建数据库。你可以借助 migrations 工具来实现这一点,当然也可以提前使用简单的脚本来完成。
静态配置
在您的 Spring Boot 配置中配置要使用的数据库名称,如下所示(当然,相同的属性也适用于 YML 或基于环境的配置,遵循 Spring Boot 的约定):
spring.data.neo4j.database = yourDatabase
完成该配置后,所有由 SDN 存储库实例(包括响应式和命令式)以及 ReactiveNeo4jTemplate
或 Neo4jTemplate
生成的查询都将针对数据库 yourDatabase
执行。
动态配置
提供一个类型为 Neo4jDatabaseNameProvider
或 ReactiveDatabaseSelectionProvider
的 bean,具体取决于您的 Spring 应用程序类型。
这个 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
的实例,并为每个实例提供整体健康状态的贡献。然而,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);
}
}
如果你有多个驱动程序和数据库选择提供程序,你需要为每种组合创建一个指示器
这确保了所有这些指示器都被分组在 Neo4j 下,取代默认的 Neo4j 健康指示器
这防止了各个贡献者直接显示在健康端点中
响应式变体
响应式变体基本上相同,使用响应式类型和相应的响应式基础设施类:
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 企业版 4.4+,客户端使用 4.4+ 驱动程序(org.neo4j.driver:neo4j-java-driver:4.4.0
或更高版本)。
对于命令式和响应式版本,你需要分别提供一个 UserSelectionProvider
和一个 ReactiveUserSelectionProvider
。相同的实例需要分别传递给 Neo4Client
和 Neo4jTransactionManager
以及它们的响应式变体。
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 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
}
为什么这不是默认只读的?虽然它适用于上述的派生查询(我们实际上知道它是只读的),但我们经常看到用户添加自定义
@Query
并通过MERGE
结构实现它的情况,这当然是一个写操作。自定义程序可以做各种事情,目前我们无法在这里检查是只读还是写入。
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);
});
}
}
在这里,对多个存储库的多次调用被封装在一个只读事务中。
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();
});
}
}
创建一个具有你所需特性的
TransactionTemplate
实例。当然,这也可以是一个全局的 bean。使用事务模板的第一个原因:声明式事务在包私有或私有方法中不起作用,在内部方法调用中也不起作用(想象一下此服务中的另一个方法调用
internalOperation
),因为它们是通过 Aspects 和代理实现的。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
}
}
让 Spring 注入这些内容
这个提供者可以是任何包含你想要引入系统的最新书签的内容
使用它创建书签管理器
将其传递给自定义的事务管理器
除非你的应用程序需要访问或提供这些数据,否则无需执行上述任何操作。如果有疑问,请不要执行任何操作。
我可以禁用书签管理吗?
我们提供了一个 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 通用注解 | 用途 | 区别 |
---|---|---|---|
org.springframework.data.neo4j.core.schema.Id | org.springframework.data.annotation.Id | 将注解的属性标记为唯一标识符。 | 特定注解没有额外的功能。 |
org.springframework.data.neo4j.core.schema.Node | org.springframework.data.annotation.Persistent | 将类标记为持久化实体。 | @Node 允许自定义标签 |
如何使用分配的 ID?
只需使用 @Id
而不使用 @GeneratedValue
,并通过构造函数参数、setter 或 wither 来填充你的 id 属性。关于如何找到合适的 id,可以参考这篇博客文章中的一些一般性建议。
如何使用外部生成的 ID?
我们提供了接口 org.springframework.data.neo4j.core.schema.IdGenerator
。你可以以任何方式实现它,并像这样配置你的实现:
@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
作为参数外,不需要提供其他内容。但是,你必须确保自定义查询能够处理 Pageable
。Pages 和 Slices 部分会为你概述所需的准备工作。
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
}
一个派生的查找器方法,它为你创建查询。它会自动处理
Pageable
。你应该使用一个已排序的Pageable
。这个方法使用
@Query
来定义一个自定义查询。它返回一个Slice<Person>
。Slice
不知道总页数,因此自定义查询不需要专门的计数查询。SDN 会通知你它正在估计下一个Slice
。Cypher 模板必须同时识别$skip
和$limit
Cypher 参数。如果你省略它们,SDN 会发出警告。这可能不符合你的预期。此外,Pageable
应该是未排序的,并且你应该提供一个稳定的排序。我们不会使用来自Pageable
的排序信息。这个方法返回一个页面。页面知道确切的页数。因此,你必须指定一个额外的计数查询。第二个方法中的所有其他限制同样适用。
我可以映射命名路径吗?
在 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
的领域类时,如 “Bacon” 距离 所示
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
函数返回的多个路径记录。
命名路径不仅可以用于高效地填充和返回根节点,还可以用于更多场景,详情请参阅附录/custom-queries.adoc#custom-query.paths。
@Query
是使用自定义查询的唯一方式吗?
不,@Query
并不是运行自定义查询的唯一方式。在你的自定义查询完全填充你的领域模型时,使用这个注解会很方便。请记住,SDN 假设你的映射领域模型是事实。这意味着如果你通过 @Query
使用了一个只部分填充模型的自定义查询,你可能会遇到使用同一个对象写回数据的危险,最终可能会擦除或覆盖你在查询中没有考虑到的数据。
因此,在所有结果形状与领域模型相同或您确定不会为写命令使用部分映射模型的情况下,请使用存储库和带有 @Query
的声明性方法。
有哪些替代方案?
-
投影可能已经足够塑造你对图的视图:它们可以用于以明确的方式定义获取属性和相关实体的深度:通过建模来实现。
-
如果你的目标是仅使查询的条件动态化,那么可以查看 QuerydslPredicateExecutor,但尤其是我们自己的变体
CypherdslConditionExecutor
。这两个 mixin 允许向我们为你创建的完整查询添加条件。因此,你将完全填充域以及自定义条件。当然,你的条件必须与我们生成的内容兼容。在此处查找根节点、相关节点等的名称 here。 -
通过
CypherdslStatementExecutor
或ReactiveCypherdslStatementExecutor
使用 Cypher-DSL。Cypher-DSL 天生适合创建动态查询。最终,这正是 SDN 在底层使用的内容。相应的 mixin 既适用于仓库本身的域类型,也适用于投影(添加条件的 mixin 不支持的)。
如果你认为可以通过部分动态查询或完整动态查询结合投影来解决你的问题,请立即跳转回章节 关于 Spring Data Neo4j Mixins。
为什么现在要讨论自定义存储库片段?
-
你可能会遇到更复杂的情况,需要多个动态查询,但这些查询在概念上仍属于存储库,而不是服务层
-
你的自定义查询返回的图形化结果与你的领域模型不完全匹配,因此自定义查询还应伴随自定义映射
-
你需要与驱动程序进行交互,例如批量加载不应通过对象映射进行。
假设有以下仓库声明,基本上聚合了一个基础仓库加上 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
}
}
Neo4jTemplate
由运行时通过DomainResultsImpl
的构造函数注入。无需使用@Autowired
。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
}
这是一个虚构的非领域结果。真实世界的查询结果可能会更加复杂。
这个片段所添加的方法。同样,该方法使用了 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
}
}
这里我们使用由基础设施提供的
Neo4jClient
。该客户端仅接收字符串,但在渲染为字符串时仍然可以使用 Cypher-DSL。
将单个值绑定到命名参数。还有一个重载方法可以绑定整个参数映射。
这是你想要的返回结果的类型。
最后是
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();
}
}
}
直接与驱动程序一起工作。与所有示例一样:不需要
@Autowired
魔法。所有的代码片段实际上都是可以独立测试的。这个用例是虚构的。在这里,我们使用驱动程序管理的事务删除整个图,并返回删除的节点和关系的数量。
这种交互当然不会在 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`");
}
}
此签名是基类所必需的。取
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);
}
}
如果希望在创建时也写入修改数据,请将其设置为 true
使用此属性指定提供审计者(即用户名)的 bean 名称
使用此属性指定提供当前日期的 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.3</version>
</dependency>
Gradle 设置的坐标是相同的。
要选择不同的数据库 —— 无论是静态还是动态地 —— 你可以添加一个 DatabaseSelectionProvider
类型的 Bean,具体方法在 Neo4j 4 支持多个数据库 - 如何使用它们? 中进行了说明。对于响应式场景,我们提供了 ReactiveDatabaseSelectionProvider
。
在没有 Spring Boot 的情况下在 Spring 上下文中使用 Spring Data Neo4j
我们提供了两个抽象的配置类,以帮助您引入所需的 beans: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");
}
}
驱动程序 bean 是必需的。
这会静态选择一个名为
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");
}
}
与普通 Spring 中的 启用 Spring Data Neo4j 基础设施以进行命令式数据库访问 相同,但使用相应的 CDI 基础设施进行注解。
这是可选的。但是,如果您运行自定义数据库选择提供程序,则不得限定此 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();
}
}
}