mcpField
N
neo4j-spring-data-skill

neo4j-spring-data-skill

github.com/neo4j-contrib开发118394 次安装1 周前更新
代码仓库

关于 neo4j-spring-data-skill

面向使用 Spring Data Neo4j 构建 Spring 应用的开发者,涵盖实体映射、仓库查询、事务及响应式访问等实践。还包含 Spring AI Neo4jVectorStore 的配置与使用指导。

主要功能

  1. 01Spring Boot 与 Neo4j 配置
  2. 02节点和关系映射
  3. 03仓库查询与投影
  4. 04响应式仓库模式
  5. 05事务管理实践
  6. 06Neo4j 向量搜索配置

适用场景

  1. 01配置 Spring 应用连接 Neo4j
  2. 02实现图实体及关系映射
  3. 03编写带分页的 Cypher 仓库查询
  4. 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

版本矩阵

SDNSpring BootSpring FrameworkJavaNeo4j
8.0.x3.3.x / 3.4.x6.2.x17+5.15+
8.1.x3.4.x+7.0.x17+5.15+
7.5.x3.2.x6.1.x17+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同一上下文中存在两种仓库类型选择一种技术栈
投影字段为 nullGetter 名称不匹配使 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。


参考资料


检查清单

  • @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)

评论

支持 Markdown