关于 spring-data-neo4j
为 Spring Boot 应用集成 Spring Data Neo4j 提供实践指南,涵盖图实体映射、命令式与响应式仓库、自定义 Cypher 查询和嵌入式数据库测试。
主要功能
- 01节点与关系实体映射
- 02命令式与响应式仓库
- 03使用 @Query 编写自定义 Cypher
- 04使用 Neo4j Harness 进行集成测试
适用场景
- 01为 Spring Boot 应用接入 Neo4j 图存储
- 02通过自定义 Cypher 查询关联节点
- 03使用嵌入式数据库测试仓库
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill spring-data-neo4j安装后在 Claude Code 中输入/spring-data-neo4j确认已加载。
附带文件references/examples.mdreferences/reference.md
详细介绍
Spring Data Neo4j 集成模式
概述
为 Spring Boot 应用提供 Spring Data Neo4j 集成模式。涵盖使用 @Node 和 @Relationship 映射节点实体、配置仓库(命令式和响应式)、使用 @Query 编写自定义 Cypher 查询,以及使用嵌入式 Neo4j 数据库进行集成测试。
适用场景
在以下情况下使用此技能:
- 在 Spring Boot 中使用图数据库和 Neo4j 集成
- 使用节点实体、关系和 Cypher 查询
- 使用 Spring Data Neo4j 仓库(命令式或响应式)
- 使用嵌入式数据库测试 Neo4j
说明
1. 设置 Spring Data Neo4j
添加依赖:
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
在 application.properties 中配置连接:
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret
配置 Cypher-DSL 方言(推荐):
@Configuration
public class Neo4jConfig {
@Bean
Configuration cypherDslConfiguration() {
return Configuration.newConfig()
.withDialect(Dialect.NEO4J_5).build();
}
}
验证检查点:继续之前,使用 cypher-shell 运行
MATCH (n) RETURN count(n),验证连接是否正常。
2. 定义节点实体
- 使用
@Node 注解标记实体类 - 选择 ID 策略:
- 将业务键用作
@Id(不可变的自然标识符) - 使用生成的
@Id@GeneratedValue(Neo4j 内部 ID)
- 将业务键用作
- 使用
@Relationship 注解定义关系 - 使用 final 字段保持实体不可变
- 使用
@Property 指定自定义属性名称
验证检查点:如果实体保存失败,请检查是否违反约束——重复 ID 会违反唯一性约束。
3. 创建仓库
- 扩展仓库接口:
- 命令式操作使用
Neo4jRepository<Entity, ID> - 响应式操作使用
ReactiveNeo4jRepository<Entity, ID>
- 命令式操作使用
- 简单查询使用查询派生
- 复杂 Cypher 查询使用
@Query 注解 - 参数使用
$paramName 语法
验证检查点:先使用
findAll()测试仓库——如果结果为空,请确认 Neo4j 实例正在运行且凭据正确。
4. 测试实现
- 使用
@DataNeo4jTest通过测试切片测试仓库 - 使用嵌入式数据库和测试夹具设置 Neo4j Harness
- 通过
withFixture()Cypher 查询提供测试数据 - 在各项测试之间清理测试数据
验证检查点:如果测试因 "Connection refused" 失败,请确保嵌入式 Neo4j 已在
@BeforeAll中成功启动。
基本实体映射
使用业务键的节点实体
@Node("Movie")
public class MovieEntity {
@Id
private final String title; // Business key as ID
@Property("tagline")
private final String description;
private final Integer year;
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
private List<Roles> actorsAndRoles = new ArrayList<>();
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();
public MovieEntity(String title, String description, Integer year) {
this.title = title;
this.description = description;
this.year = year;
}
}
使用生成 ID 的节点实体
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private final String title;
@Property("tagline")
private final String description;
public MovieEntity(String title, String description) {
this.id = null; // Never set manually
this.title = title;
this.description = description;
}
// Wither method for immutability with generated IDs
public MovieEntity withId(Long id) {
if (this.id != null && this.id.equals(id)) {
return this;
} else {
MovieEntity newObject = new MovieEntity(this.title, this.description);
newObject.id = id;
return newObject;
}
}
}
仓库模式
基本仓库接口
@Repository
public interface MovieRepository extends Neo4jRepository<MovieEntity, String> {
// Query derivation from method name
MovieEntity findOneByTitle(String title);
List<MovieEntity> findAllByYear(Integer year);
List<MovieEntity> findByYearBetween(Integer startYear, Integer endYear);
}
响应式仓库
@Repository
public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {
Mono<MovieEntity> findOneByTitle(String title);
Flux<MovieEntity> findAllByYear(Integer year);
}
命令式与响应式:
- 阻塞式命令式操作使用
Neo4jRepository - 非阻塞式响应式操作使用
ReactiveNeo4jRepository - 不要在同一个应用中混用命令式和响应式
- 响应式要求数据库端使用 Neo4j 4+
使用 @Query 编写自定义查询
@Repository
public interface AuthorRepository extends Neo4jRepository<Author, Long> {
@Query("MATCH (b:Book)-[:WRITTEN_BY]->(a:Author) " +
"WHERE a.name = $name AND b.year > $year " +
"RETURN b")
List<Book> findBooksAfterYear(@Param("name") String name,
@Param("year") Integer year);
@Query("MATCH (b:Book)-[:WRITTEN_BY]->(a:Author) " +
"WHERE a.name = $name " +
"RETURN b ORDER BY b.year DESC")
List<Book> findBooksByAuthorOrderByYearDesc(@Param("name") String name);
}
自定义查询最佳实践:
- 使用
$parameterName作为参数占位符 - 当参数名称与方法参数不同时,使用
@Param注解 - MATCH 用于指定节点模式和关系
- WHERE 用于筛选结果
- RETURN 用于定义返回内容
测试策略
使用 Neo4j Harness 进行集成测试
测试配置:
@DataNeo4jTest
class BookRepositoryIntegrationTest {
private static Neo4j embeddedServer;
@BeforeAll
static void initializeNeo4j() {
embeddedServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer() // No HTTP access needed
.withFixture(
"CREATE (b:Book {isbn: '978-0547928210', " +
"name: 'The Fellowship of the Ring', year: 1954})" +
"-[:WRITTEN_BY]->(a:Author {id: 1, name: 'J. R. R. Tolkien'}) " +
"CREATE (b2:Book {isbn: '978-0547928203', " +
"name: 'The Two Towers', year: 1956})" +
"-[:WRITTEN_BY]->(a)"
)
.build();
}
@AfterAll
static void stopNeo4j() {
embeddedServer.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> "null");
}
@Autowired
private BookRepository bookRepository;
@Test
void givenBookExists_whenFindOneByTitle_thenBookIsReturned() {
Book book = bookRepository.findOneByTitle("The Fellowship of the Ring");
assertThat(book.getIsbn()).isEqualTo("978-0547928210");
}
}
示例
示例 1:保存和检索实体
输入:
MovieEntity movie = new MovieEntity("The Matrix", "Welcome to the Real World", 1999);
movieRepository.save(movie);
MovieEntity found = movieRepository.findOneByTitle("The Matrix");
输出:
MovieEntity{
title="The Matrix",
description="Welcome to the Real World",
year=1999,
actorsAndRoles=[],
directors=[]
}
示例 2:自定义 Cypher 查询
输入:
List<Book> books = authorRepository.findBooksAfterYear("J.R.R. Tolkien", 1950);
输出:
[
Book{isbn="978-0547928210", name="The Fellowship of the Ring", year=1954},
Book{isbn="978-0547928203", name="The Two Towers", year=1956},
Book{isbn="978-0547928227", name="The Return of the King", year=1957}
]
示例 3:关系遍历
输入:
@Query("MATCH (m:Movie)<-[:ACTED_IN]-(a:Person) " +
"WHERE m.title = $title RETURN a.name as actorName")
List<String> findActorsByMovieTitle(@Param("title") String title);
List<String> actors = movieRepository.findActorsByMovieTitle("The Matrix");
输出:
["Keanu Reeves", "Laurence Fishburne", "Carrie-Anne Moss", "Hugo Weaving"]
从基础示例逐步深入高级示例,涵盖完整的电影数据库、社交网络模式、电商产品目录、自定义查询和响应式操作。
请参阅 示例,了解完整的代码示例。
最佳实践
实体设计
- 使用带有 final 字段的不可变实体
- 选择业务键(
@Id)或生成的 ID(@Id@GeneratedValue) - 让实体专注于图结构,而非业务逻辑
- 使用恰当的关系方向(INCOMING、OUTGOING、UNDIRECTED)
仓库设计
- 命令式应用扩展
Neo4jRepository,响应式应用扩展ReactiveNeo4jRepository - 简单查询使用查询派生
- 复杂图模式编写自定义
@Query - 不要在同一个应用中混用命令式和响应式
配置
- 始终显式配置 Cypher-DSL 方言
- 使用特定于环境的属性设置凭据
- 不要在源代码中硬编码凭据
- 根据负载配置连接池
测试
- 使用 Neo4j Harness 进行集成测试
- 通过
withFixture()Cypher 查询提供测试数据 - 使用
@DataNeo4jTest进行测试切片 - 测试成功场景和边缘场景
架构
- 仅使用构造函数注入
- 将领域实体与 DTO 分开
- 遵循基于功能的包结构
- 保持领域层与框架无关
安全性
- 使用 Spring Boot 属性覆盖凭据
- 配置适当的身份验证和授权
- 在服务层验证输入参数
- 使用参数化查询防止 Cypher 注入
限制和警告
- 不要在同一个应用中混用命令式和响应式仓库。
- Neo4j 写操作需要事务;确保正确配置
@Transactional。 - 谨慎进行深层关系遍历,因为这可能导致性能问题。
- 应对大型结果集进行分页,以避免内存问题。
- Cypher 查询区分大小写;确保属性名称的大小写一致。
- 不可变实体需要正确的 wither 方法来处理生成的 ID。
- Spring Data Neo4j 中的关系默认不会延迟加载;对于大型图,建议使用投影。
- Neo4j Java 驱动程序与响应式流不兼容;响应式操作请使用响应式驱动程序。
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
在 localhost:7687 上出现 Connection refused | Neo4j 服务未运行 | 启动 Neo4j,或在测试中使用嵌入式 Neo4j |
Authentication failed | 凭据错误 | 检查 spring.neo4j.authentication.username/password |
实体未保存 / MATCH 未返回任何结果 | 事务未提交 | 添加 @Transactional 或检查自动提交设置 |
保存时出现 ConstraintViolationException | @Id 值重复 | 确保 ID 唯一,或使用 @GeneratedValue |
| 结果中缺少关系 | @Relationship 方向错误 | 检查 Direction.INCOMING/OUTGOING/UNDIRECTED |
@Query 返回错误数据 | Cypher 参数语法错误 | 使用 $paramName,而非 $ {paramName} |
@DataNeo4jTest 测试失败 | 嵌入式 Neo4j 未启动 | 确保 @BeforeAll 在测试前启动 Neo4j |
参考资料
有关完整 API 参考、Cypher 查询模式和配置选项的详细文档: