关于 neo4j-spring-data-skill
面向使用 Spring Data Neo4j 构建 Spring 应用的开发者,涵盖实体映射、仓库查询、事务及响应式访问等实践。还包含 Spring AI Neo4jVectorStore 的配置与使用指导。
主要功能
- 01Spring Boot 与 Neo4j 配置
- 02节点和关系映射
- 03仓库查询与投影
- 04响应式仓库模式
- 05事务管理实践
- 06Neo4j 向量搜索配置
适用场景
- 01配置 Spring 应用连接 Neo4j
- 02实现图实体及关系映射
- 03编写带分页的 Cypher 仓库查询
- 04在 Spring AI 中使用 Neo4j 向量搜索
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-spring-data-skill安装后在 Claude Code 中输入/neo4j-spring-data-skill确认已加载。
附带文件README.mdreferences/modeling-pitfalls.md
详细介绍
Neo4j Spring Data 技能
适用场景
- 使用 Neo4j 配置 Spring Boot(
spring-boot-starter-data-neo4j) - 编写
@Node实体类和@Relationship/@RelationshipProperties映射 - 定义
Neo4jRepository或ReactiveNeo4jRepository接口 - 在仓库方法上使用 Cypher 编写
@Query注解 - 在 Neo4j 中使用 Spring 投影(基于接口、DTO、动态投影)
- 配置 Neo4j 连接的
application.yml - 通过
Neo4jClient或Neo4jTemplate编写自定义查询 - 在 Spring 应用中使用 Spring AI
Neo4jVectorStore进行向量搜索 - 事务管理、审计、乐观锁
不适用场景
- 不使用 Spring 的原始 Java 驱动 →
neo4j-driver-java-skill - Cypher 查询编写 →
neo4j-cypher-skill - 驱动版本升级 →
neo4j-migration-skill - GDS 算法 →
neo4j-gds-skill
版本矩阵
| SDN | Spring Boot | Spring Framework | Java | Neo4j |
|---|---|---|---|---|
| 8.0.x | 3.3.x / 3.4.x | 6.2.x | 17+ | 5.15+ |
| 8.1.x | 3.4.x+ | 7.0.x | 17+ | 5.15+ |
| 7.5.x | 3.2.x | 6.1.x | 17+ | 4.4+ |
使用 spring-boot-starter-data-neo4j — 它会引入 SDN 和驱动。使用 Spring Boot BOM 时,无需显式指定 SDN 版本。
设置
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'
响应式技术栈(与上述配置一同添加)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
配置
application.yml — 命令式(标准)
spring:
neo4j:
uri: ${NEO4J_URI:bolt://localhost:7687}
authentication:
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: ${NEO4J_DATABASE:neo4j}
application.yml — Aura(需要 TLS)
spring:
neo4j:
uri: ${NEO4J_URI} # neo4j+s://xxxx.databases.neo4j.io
authentication:
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: ${NEO4J_DATABASE:neo4j}
凭据:存储在 .env 中;切勿硬编码。确认 .env 已列入 .gitignore。
实体映射
import org.springframework.data.neo4j.core.schema.*;
// Internal generated ID (default for most cases)
@Node("Person")
public class PersonEntity {
@Id @GeneratedValue private Long id; // element ID (Long)
private String name;
@Property("birth_year") private Integer birthYear; // custom property name
@Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
private List<PersonEntity> friends = new ArrayList<>();
}
// UUID business key
@Node("Product")
public class ProductEntity {
@Id @GeneratedValue(generatorClass = GeneratedValue.UUIDStringGenerator.class)
private String id;
@Version private Long version; // optimistic locking; required with business key
}
// User-assigned key (caller sets value; no @GeneratedValue)
@Node("Country")
public class CountryEntity {
@Id private String isoCode;
private String name;
}
// Multiple static labels
@Node(primaryLabel = "Vehicle", labels = {"Car", "Auditable"})
public class CarEntity { ... }
// Runtime labels
@Node("Content")
public class ContentEntity {
@Id @GeneratedValue private Long id;
@DynamicLabels private Set<String> tags = new HashSet<>(); // labels added at runtime
}
关系属性
当关系本身携带数据时,使用 @RelationshipProperties。
@RelationshipProperties
public class RolesRelationship {
@RelationshipId // internal relationship ID; required
private Long id;
private List<String> roles;
@TargetNode // marks the other end of the relationship
private PersonEntity person;
}
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private String title;
@Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
private List<RolesRelationship> actorsAndRoles = new ArrayList<>();
}
仓库接口
基本 CRUD
import org.springframework.data.neo4j.repository.Neo4jRepository;
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long> {
Optional<PersonEntity> findByName(String name);
List<PersonEntity> findByBirthYearBetween(int from, int to);
List<PersonEntity> findByNameContainingIgnoreCase(String fragment);
long countByBirthYearGreaterThan(int year);
void deleteByName(String name);
}
@Query — 自定义 Cypher
// CORRECT: $param bound parameter
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f:Person) RETURN f")
List<PersonEntity> findFriendsOf(String name);
// With pagination
@Query(value = "MATCH (p:Person) RETURN p ORDER BY p.name",
countQuery = "MATCH (p:Person) RETURN count(p)")
Page<PersonEntity> findAllPaged(Pageable pageable);
// Return relationship-rich entity; map target via @Node return
@Query("MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person {name: $name}) RETURN m, collect(r), collect(p)")
List<MovieEntity> findMoviesActedInBy(String name);
安全规则:绝不要将用户输入拼接到 Cypher 字符串中。始终使用 $paramName。
分页和排序
Page<PersonEntity> findByBirthYearGreaterThan(int year, Pageable pageable);
List<PersonEntity> findTop10ByOrderByNameAsc();
List<PersonEntity> findByName(String name, Sort sort);
用法:
Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending());
Page<PersonEntity> result = repo.findByBirthYearGreaterThan(1980, page);
投影
接口投影(封闭式 — 可优化查询)
public interface PersonSummary {
String getName();
Integer getBirthYear();
}
List<PersonSummary> findByBirthYearLessThan(int year);
DTO 投影(record — Java 17+ 中的首选方式)
public record PersonDto(String name, Integer birthYear) {}
List<PersonDto> findByName(String name);
动态投影
<T> List<T> findByName(String name, Class<T> type);
// Usage
repo.findByName("Alice", PersonSummary.class);
repo.findByName("Alice", PersonEntity.class);
开放式投影 — SpEL(会禁用查询优化)
public interface FullName {
@Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName();
}
响应式仓库
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ReactivePersonRepository extends ReactiveNeo4jRepository<PersonEntity, Long> {
Mono<PersonEntity> findByName(String name);
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f")
Flux<PersonEntity> findFriendsOf(String name);
}
切勿在同一个应用上下文中混用命令式和响应式数据库访问。
自定义仓库实现
片段模式 — 在 @Query 无法满足需求时使用。
// 1. Fragment interface
public interface PersonRepositoryCustom {
List<PersonEntity> findByComplexCriteria(String criteria);
}
// 2. Impl — must end with "Impl"
public class PersonRepositoryCustomImpl implements PersonRepositoryCustom {
private final Neo4jClient neo4jClient;
PersonRepositoryCustomImpl(Neo4jClient c) { this.neo4jClient = c; }
@Override
public List<PersonEntity> findByComplexCriteria(String c) {
return new ArrayList<>(neo4jClient
.query("MATCH (p:Person) WHERE p.name CONTAINS $c RETURN p").bind(c).to("c")
.fetchAs(PersonEntity.class)
.mappedBy((t, r) -> { var e = new PersonEntity(); e.setName(r.get("p").asNode().get("name").asString()); return e; })
.all());
}
}
// 3. Compose
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long>, PersonRepositoryCustom {}
Neo4jClient — 低级查询
当 @Query 无法满足需求,或需要完全控制 Cypher 执行时使用。
// Bind params + fetch single scalar
neo4jClient.query("MATCH (p:Person {name: $name}) RETURN count(*) AS cnt")
.bind("Alice").to("name")
.fetchAs(Long.class)
.mappedBy((t, r) -> r.get("cnt").asLong())
.one();
// Bind + run write (no result)
neo4jClient.query("MERGE (p:Person {name: $name})")
.bind(personName).to("name")
.run();
// Custom object mapping
neo4jClient.query("MATCH (p:Person)-[:DIRECTED]->(m:Movie) WHERE p.name=$n RETURN p, collect(m) AS movies")
.bind("Lilly Wachowski").to("n")
.fetchAs(Director.class)
.mappedBy((typeSystem, record) -> new Director(
record.get("p").asNode().get("name").asString(),
record.get("movies").asList(v -> new Movie(v.get("title").asString()))
)).one();
完整 API:references/neo4j-client.md
事务管理
@Service
@Transactional // class-level: all methods transactional
public class PersonService {
@Transactional(readOnly = true) // read-only hint
public Optional<PersonEntity> findByName(String name) { ... }
@Transactional // explicit write
public PersonEntity save(PersonEntity p) { return repository.save(p); }
}
Neo4jTransactionManager 会自动配置。若未使用显式限定符,切勿与 JPA PlatformTransactionManager 混用。应在具体类上使用 @Transactional,而非接口。
Spring AI — Neo4jVectorStore
依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-neo4j</artifactId>
</dependency>
application.yml
spring:
ai:
vectorstore:
neo4j:
initialize-schema: true # creates vector index on first run
index-name: my-index
embedding-dimension: 1536 # must match your embedding model
distance-type: cosine # cosine (default) or euclidean
label: Document # node label for stored chunks
embedding-property: embedding # property for the vector
需要 Neo4j 5.15+。复用 spring.neo4j.* 连接配置。
用法
@Autowired VectorStore vectorStore;
// Store
vectorStore.add(List.of(new Document("text", Map.of("author", "alice"))));
// Similarity search
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5).similarityThreshold(0.75).build()
);
// With metadata filter
vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5)
.filterExpression("author == 'alice'").build()
);
常见错误
| 错误 | 原因 | 修复方法 |
|---|---|---|
MappingException: Could not find entity | 未扫描实体 | 检查 @EnableNeo4jRepositories 的基础包 |
| 加载后关系为 null | 默认深度可能会跳过较深的关系 | 使用带有 RETURN m, collect(r), collect(p) 的 @Query |
| N+1 查询 | 逐个实体获取关系 | 改用单个 @Query;使用投影 |
OptimisticLockingFailureException | 并发写入时 @Version 过期 | 在服务层重试 |
IllegalStateException: Cannot mix reactive/imperative | 同一上下文中存在两种仓库类型 | 选择一种技术栈 |
| 投影字段为 null | Getter 名称不匹配 | 使 Getter 与属性名称一致;检查 @Property 别名 |
带关系的 @Query 结果为空 | 缺少 collect(r), collect(p) | 同时返回根节点、关系和相关节点 |
Cannot delete node, node has relationships | 使用 deleteById 时未执行 detach | 使用带有 DETACH DELETE 的 @Query |
| 事务未回滚 | @Transactional 标注在接口上 | 应用于具体的服务类 |
关系加载 — 关键规则
SDN 会按配置的深度(默认:1 跳)急切加载相关实体。对于更深的图:
// Explicit @Query to control what gets loaded
@Query("""
MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person)
WHERE m.title = $title
RETURN m, collect(r), collect(p)
""")
Optional<MovieEntity> findByTitleWithCast(String title);
RETURN 中必须包含 collect(r), collect(p),SDN 才能正确映射 @RelationshipProperties。
参考资料
- Spring Data Neo4j 参考文档(8.x)
- Spring AI Neo4jVectorStore
- GraphAcademy:使用 Spring Data 构建 Neo4j 应用
- Neo4j 入门 — SDN
- SDN 高级投影
- SDN 审计
- 建模陷阱、投影指南、类型映射
检查清单
-
@Node使用明确的标签字符串,而非默认类名 - 使用
@Id @GeneratedValue(或为带乐观锁的业务键使用@Id+@Version) -
@RelationshipProperties类包含@RelationshipId和@TargetNode - 明确指定
@Relationship方向(OUTGOING / INCOMING) -
@Query中的 Cypher 使用$paramName,不进行字符串拼接 - 包含丰富关系的
@Query在返回根节点的同时,也返回collect(r), collect(p) - 在
application.yml中设置数据库名称(避免默认数据库不明确) - 数据库中为仓库查询使用的所有业务键创建唯一约束
- 将
@Transactional标注在具体的服务类上(而非接口上) - 同一应用上下文中不混用命令式和响应式模式
- 凭据放在环境变量中;将
.env加入.gitignore - 首次运行时设置
spring.ai.vectorstore.neo4j.initialize-schema: true(Spring AI)