neo4j-spring-data-skill
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNeo4j Spring Data Skill
Neo4j Spring Data 技能指南
When to Use
适用场景
- Configuring Spring Boot with Neo4j ()
spring-boot-starter-data-neo4j - Writing entity classes and
@Node/@Relationshipmappings@RelationshipProperties - Defining or
Neo4jRepositoryinterfacesReactiveNeo4jRepository - Writing annotations with Cypher on repository methods
@Query - Using Spring projections (interface-based, DTO, dynamic) with Neo4j
- Configuring for Neo4j connection
application.yml - Custom queries via or
Neo4jClientNeo4jTemplate - Spring AI for vector search in Spring apps
Neo4jVectorStore - Transaction management, auditing, optimistic locking
- 配置集成Neo4j的Spring Boot应用(使用依赖)
spring-boot-starter-data-neo4j - 编写实体类及
@Node/@Relationship映射@RelationshipProperties - 定义或
Neo4jRepository接口ReactiveNeo4jRepository - 在仓库方法上使用带Cypher语句的注解
@Query - 将Spring投影(基于接口、DTO、动态投影)与Neo4j结合使用
- 配置中的Neo4j连接信息
application.yml - 通过或
Neo4jClient执行自定义查询Neo4jTemplate - 在Spring应用中使用Spring AI 实现向量搜索
Neo4jVectorStore - 事务管理、审计、乐观锁机制
When NOT to Use
不适用场景
- Raw Java driver without Spring →
neo4j-driver-java-skill - Cypher query authoring →
neo4j-cypher-skill - Driver version upgrades →
neo4j-migration-skill - GDS algorithms →
neo4j-gds-skill
- 未集成Spring的原生Java驱动 → 使用
neo4j-driver-java-skill - Cypher查询编写 → 使用
neo4j-cypher-skill - 驱动版本升级 → 使用
neo4j-migration-skill - GDS算法 → 使用
neo4j-gds-skill
Version Matrix
版本对应矩阵
| 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+ |
Use — it pulls SDN + driver. No explicit SDN version needed when using Spring Boot BOM.
spring-boot-starter-data-neo4j| 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+ |
推荐使用依赖——它会自动引入SDN及对应驱动。使用Spring Boot BOM时无需显式指定SDN版本。
spring-boot-starter-data-neo4jSetup
环境搭建
Maven
Maven依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>Gradle
Gradle依赖
gradle
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'gradle
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'Reactive stack (add alongside above)
响应式栈(需搭配上述依赖)
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>Configuration
配置示例
application.yml — imperative (standard)
application.yml — 命令式(标准)
yaml
spring:
neo4j:
uri: ${NEO4J_URI:bolt://localhost:7687}
authentication:
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: ${NEO4J_DATABASE:neo4j}yaml
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 required)
application.yml — Aura环境(需TLS)
yaml
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}Credentials: store in ; never hardcode. Verify is in .
.env.env.gitignoreyaml
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.gitignoreEntity Mapping
实体映射示例
java
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
}java
import org.springframework.data.neo4j.core.schema.*;
// 内部生成ID(大多数场景下的默认选项)
@Node("Person")
public class PersonEntity {
@Id @GeneratedValue private Long id; // 元素ID(Long类型)
private String name;
@Property("birth_year") private Integer birthYear; // 自定义属性名称
@Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
private List<PersonEntity> friends = new ArrayList<>();
}
// UUID业务主键
@Node("Product")
public class ProductEntity {
@Id @GeneratedValue(generatorClass = GeneratedValue.UUIDStringGenerator.class)
private String id;
@Version private Long version; // 乐观锁;使用业务主键时必填
}
// 用户指定主键(调用方设置值;无需@GeneratedValue)
@Node("Country")
public class CountryEntity {
@Id private String isoCode;
private String name;
}
// 多静态标签
@Node(primaryLabel = "Vehicle", labels = {"Car", "Auditable"})
public class CarEntity { ... }
// 运行时动态标签
@Node("Content")
public class ContentEntity {
@Id @GeneratedValue private Long id;
@DynamicLabels private Set<String> tags = new HashSet<>(); // 运行时添加的标签
}Relationship Properties
关系属性示例
Use when the relationship itself carries data.
@RelationshipPropertiesjava
@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;
}java
@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<>();
}当关系本身携带数据时,使用。
@RelationshipPropertiesjava
@RelationshipProperties
public class RolesRelationship {
@RelationshipId // 内部关系ID;必填
private Long id;
private List<String> roles;
@TargetNode // 标记关系的另一端节点
private PersonEntity person;
}java
@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<>();
}Repository Interfaces
仓库接口示例
Basic CRUD
基础CRUD操作
java
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);
}java
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 — custom Cypher
@Query — 自定义Cypher语句
java
// 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);Security rule: NEVER string-concatenate user input into Cypher. Always use .
$paramNamejava
// 正确写法:使用$param绑定参数
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f:Person) RETURN f")
List<PersonEntity> findFriendsOf(String name);
// 分页查询
@Query(value = "MATCH (p:Person) RETURN p ORDER BY p.name",
countQuery = "MATCH (p:Person) RETURN count(p)")
Page<PersonEntity> findAllPaged(Pageable pageable);
// 返回包含关系的实体;通过@Node返回映射目标节点
@Query("MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person {name: $name}) RETURN m, collect(r), collect(p)")
List<MovieEntity> findMoviesActedInBy(String name);安全规则:切勿将用户输入字符串拼接进Cypher语句。始终使用参数绑定。
$paramNamePagination and sorting
分页与排序
java
Page<PersonEntity> findByBirthYearGreaterThan(int year, Pageable pageable);
List<PersonEntity> findTop10ByOrderByNameAsc();
List<PersonEntity> findByName(String name, Sort sort);Usage:
java
Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending());
Page<PersonEntity> result = repo.findByBirthYearGreaterThan(1980, page);java
Page<PersonEntity> findByBirthYearGreaterThan(int year, Pageable pageable);
List<PersonEntity> findTop10ByOrderByNameAsc();
List<PersonEntity> findByName(String name, Sort sort);使用示例:
java
Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending());
Page<PersonEntity> result = repo.findByBirthYearGreaterThan(1980, page);Projections
投影示例
Interface projection (closed — query-optimizable)
接口投影(封闭型——支持查询优化)
java
public interface PersonSummary {
String getName();
Integer getBirthYear();
}
List<PersonSummary> findByBirthYearLessThan(int year);java
public interface PersonSummary {
String getName();
Integer getBirthYear();
}
List<PersonSummary> findByBirthYearLessThan(int year);DTO projection (record — preferred in Java 17+)
DTO投影(记录类型——Java 17+推荐使用)
java
public record PersonDto(String name, Integer birthYear) {}
List<PersonDto> findByName(String name);java
public record PersonDto(String name, Integer birthYear) {}
List<PersonDto> findByName(String name);Dynamic projection
动态投影
java
<T> List<T> findByName(String name, Class<T> type);
// Usage
repo.findByName("Alice", PersonSummary.class);
repo.findByName("Alice", PersonEntity.class);java
<T> List<T> findByName(String name, Class<T> type);
// 使用示例
repo.findByName("Alice", PersonSummary.class);
repo.findByName("Alice", PersonEntity.class);Open projection — SpEL (disables query optimization)
开放投影——SpEL(会禁用查询优化)
java
public interface FullName {
@Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName();
}java
public interface FullName {
@Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName();
}Reactive Repository
响应式仓库
java
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);
}Do NOT mix imperative and reactive database access in the same application context.
java
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);
}请勿在同一应用上下文中混合使用命令式与响应式数据库访问方式。
Custom Repository Implementation
自定义仓库实现
Fragment pattern — use when is not enough.
@Queryjava
// 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 {}片段模式——当无法满足需求时使用。
@Queryjava
// 1. 片段接口
public interface PersonRepositoryCustom {
List<PersonEntity> findByComplexCriteria(String criteria);
}
// 2. 实现类——名称必须以"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. 组合仓库接口
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long>, PersonRepositoryCustom {}Neo4jClient — Low-Level Queries
Neo4jClient — 底层查询
Use when is insufficient or you need full control over Cypher execution.
@Queryjava
// 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();Full API: references/neo4j-client.md
当不足以满足需求或需要完全控制Cypher执行逻辑时使用。
@Queryjava
// 绑定参数并获取单个标量结果
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();
// 绑定参数并执行写入操作(无返回结果)
neo4jClient.query("MERGE (p:Person {name: $name})")
.bind(personName).to("name")
.run();
// 自定义对象映射
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
Transaction Management
事务管理
java
@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); }
}Neo4jTransactionManagerPlatformTransactionManager@Transactionaljava
@Service
@Transactional // 类级别:所有方法均为事务性
public class PersonService {
@Transactional(readOnly = true) // 只读提示
public Optional<PersonEntity> findByName(String name) { ... }
@Transactional // 显式写入事务
public PersonEntity save(PersonEntity p) { return repository.save(p); }
}Neo4jTransactionManagerPlatformTransactionManager@TransactionalSpring AI — Neo4jVectorStore
Spring AI — Neo4jVectorStore
Dependency
依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-neo4j</artifactId>
</dependency>xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-neo4j</artifactId>
</dependency>application.yml
application.yml配置
yaml
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 vectorRequires Neo4j 5.15+. Reuses connection config.
spring.neo4j.*yaml
spring:
ai:
vectorstore:
neo4j:
initialize-schema: true # 首次运行时创建向量索引
index-name: my-index
embedding-dimension: 1536 # 必须与你的嵌入模型维度匹配
distance-type: cosine # cosine(默认)或euclidean
label: Document # 存储分片的节点标签
embedding-property: embedding # 存储向量的属性名要求Neo4j版本5.15+。复用的连接配置。
spring.neo4j.*Usage
使用示例
java
@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()
);java
@Autowired VectorStore vectorStore;
// 存储文档
vectorStore.add(List.of(new Document("text", Map.of("author", "alice"))));
// 相似度搜索
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5).similarityThreshold(0.75).build()
);
// 带元数据过滤的相似度搜索
vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5)
.filterExpression("author == 'alice'").build()
);Common Errors
常见错误排查
| Error | Cause | Fix |
|---|---|---|
| Entity not scanned | Check |
| Relationships null after load | Default depth may skip deep rels | Use |
| N+1 queries | Per-entity relationship fetch | Rewrite with single |
| Stale | Retry in service layer |
| Both repo types in same context | Pick one stack |
| Projection null fields | Getter name mismatch | Match getter to property name; check |
| Missing | Return root node + rels + related nodes together |
| | Use |
| Transaction not rolling back | | Apply on concrete service class |
| 错误信息 | 原因 | 修复方案 |
|---|---|---|
| 实体未被扫描到 | 检查 |
| 加载后关系字段为null | 默认深度可能跳过深层关系 | 使用包含 |
| N+1查询问题 | 逐实体获取关系 | 重写为单条 |
| 并发写入时 | 在服务层添加重试逻辑 |
| 同一上下文中同时存在两种类型的仓库 | 选择其中一种技术栈 |
| 投影字段为null | Getter名称不匹配 | 确保Getter与属性名称一致;检查 |
带关系的 | 缺少 | 同时返回根节点、关系及关联节点 |
| 使用 | 使用带 |
| 事务未回滚 | | 将注解应用于具体服务类 |
Relationship Loading — Key Rule
关系加载核心规则
SDN loads related entities eagerly up to a configured depth (default: 1 hop). For deeper graphs:
java
// 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);collect(r), collect(p)@RelationshipPropertiesSDN会按配置深度(默认:1跳)预加载关联实体。对于更深层次的图结构:
java
// 使用显式@Query控制加载内容
@Query("""
MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person)
WHERE m.title = $title
RETURN m, collect(r), collect(p)
""")
Optional<MovieEntity> findByTitleWithCast(String title);返回语句中的是SDN正确映射的必要条件。
collect(r), collect(p)@RelationshipPropertiesReferences
参考文档
- Spring Data Neo4j Reference (8.x)
- Spring AI Neo4jVectorStore
- GraphAcademy: Building Neo4j Apps with Spring Data
- Neo4j Getting Started — SDN
- SDN Advanced projections
- SDN Auditing
- Modeling pitfalls, projection guide, type mapping
Checklist
检查清单
- uses explicit label string, not default class name
@Node - (or
@Id @GeneratedValue+@Idfor business key with optimistic lock)@Version - class has
@RelationshipPropertiesand@RelationshipId@TargetNode - direction is explicit (OUTGOING / INCOMING)
@Relationship - Cypher uses
@Query— no string concatenation$paramName - Relationship-rich returns
@Queryalongside root nodecollect(r), collect(p) - Database name set in (avoids default DB ambiguity)
application.yml - Unique constraint exists in DB for any business key used in repository lookups
- on concrete service class (not interface)
@Transactional - No imperative + reactive mix in same application context
- Credentials in env vars; in
.env.gitignore - for first run (Spring AI)
spring.ai.vectorstore.neo4j.initialize-schema: true
- 使用显式标签字符串,而非默认类名
@Node - 使用(或
@Id @GeneratedValue+@Id实现带乐观锁的业务主键)@Version - 类包含
@RelationshipProperties和@RelationshipId@TargetNode - 显式指定方向(OUTGOING / INCOMING)
@Relationship - 中的Cypher使用
@Query参数绑定——无字符串拼接$paramName - 包含关系的返回
@Query及根节点collect(r), collect(p) - 在中设置数据库名称(避免默认数据库歧义)
application.yml - 仓库查询使用的业务主键在数据库中存在唯一约束
- 标注在具体服务类上(而非接口)
@Transactional - 同一应用上下文中未混合命令式与响应式方式
- 凭证存储在环境变量中;已加入
.env.gitignore - 首次运行Spring AI时设置
spring.ai.vectorstore.neo4j.initialize-schema: true