关于 neo4j-driver-java-skill
面向通过 Java 或 Kotlin 连接 Neo4j 的开发者的技能指南,涵盖驱动、同步与异步访问、事务和结果映射,并提供错误处理与性能建议。
主要功能
- 01配置 Neo4j Java Driver
- 02选择查询、事务及访问模式
- 03处理结果、数据类型与映射
- 04排查连接和事务错误
- 05批量写入及连接池调优
适用场景
- 01在 Java 或 Kotlin 项目中连接 Neo4j
- 02处理大型查询结果或流式读取
- 03为多方法操作管理显式事务
- 04排查连接池或结果处理问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-driver-java-skill安装后在 Claude Code 中输入/neo4j-driver-java-skill确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdreferences/advanced-config.mdasync-reactive.mdproperty-encryption.md详细介绍
适用场景
- 使用 Java/Kotlin 代码连接 Neo4j(Aura 或自行管理的实例)
- 在 Maven/Gradle 项目中配置驱动程序、会话和事务
- 调试结果处理、错误恢复和连接池问题
- 通过异步(
CompletableFuture)或响应式(Project Reactor / RxJava)方式访问 Neo4j
不适用场景
- 编写/优化 Cypher 查询 →
neo4j-cypher-skill - 升级驱动程序版本 →
neo4j-migration-skill - Spring Data Neo4j(
@Node、@Relationship、Neo4jRepository)→neo4j-spring-data-skill
依赖项
Maven
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>6.3.0</version>
</dependency>
Gradle
implementation 'org.neo4j.driver:neo4j-java-driver:6.3.0'
查看最新版本:https://central.sonatype.com/artifact/org.neo4j.driver/neo4j-java-driver
6.2.0 [2026-06]:支持 Neo4j UUID 类型和 Bolt 6.1;结果摘要中包含 QueryProfile。
6.2.1 [2026-08]:修复了 UUID 值的 Value#asObject() 问题——如果要以通用方式读取 UUID 属性,则需要此版本。Neo4j 2026.08 服务端捆绑了 6.2.1;2026.07 捆绑了 6.2.0。
6.3.0 [2026-09]:预览属性加密功能——对属性值进行客户端信封加密(driver.propertyEncryption())→ references/property-encryption.md。
服务端 UUID 类型和 uuid() 构造函数随 Neo4j 2026.08 一同发布——连接这些服务端时,请使用 >= 6.2.1 版本的驱动程序。
环境变量
连接配置的标准做法——切勿硬编码凭据:
String uri = System.getenv().getOrDefault("NEO4J_URI", "neo4j://localhost:7687");
String user = System.getenv().getOrDefault("NEO4J_USERNAME", "neo4j");
String password = System.getenv().getOrDefault("NEO4J_PASSWORD", "");
String database = System.getenv().getOrDefault("NEO4J_DATABASE", "neo4j");
Spring Boot:通过 @Value("${spring.neo4j.uri}") 或 application.properties 注入:
spring.neo4j.uri=neo4j+s://xxx.databases.neo4j.io
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret
驱动程序生命周期
每个应用程序使用一个 Driver——它是线程安全的,但创建开销较大。实现 AutoCloseable 或使用 try-with-resources。
// Long-lived singleton
var driver = GraphDatabase.driver(
"neo4j+s://xxx.databases.neo4j.io", // Aura TLS+routing
AuthTokens.basic(user, password));
driver.verifyConnectivity(); // fail fast
// Short-lived (tests / CLI)
try (var driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password))) {
driver.verifyConnectivity();
// ...
}
URI 方案:
| URI | 用途 |
|---|---|
neo4j://localhost | 不加密,集群路由 |
neo4j+s://xxx.databases.neo4j.io | TLS + 集群路由(Aura) |
bolt://localhost:7687 | 不加密,单实例 |
bolt+s://localhost:7687 | TLS,单实例 |
身份验证选项:AuthTokens.basic(u,p) · AuthTokens.bearer(token) · AuthTokens.kerberos(b64) · AuthTokens.none()
选择合适的 API
| API | 适用场景 | 自动重试 | 流式处理 |
|---|---|---|---|
driver.executableQuery() | 大多数查询的默认选择 | ✅ | ❌ 预先执行 |
session.executeRead/Write() | 大型结果集、需要回调控制 | ✅ | ✅ |
session.beginTransaction() | 多方法操作、外部协调 | ❌ | ✅ |
session.run() | 自行管理的查询(CALL IN TRANSACTIONS) | ⚠️ 一次性 [6.1+] | ✅ |
driver.asyncSession() | 非阻塞 CompletableFuture | ✅ | ✅ |
driver.rxSession() | Reactor/RxJava 背压 | ✅ | ✅ |
CALL { … } IN TRANSACTIONS 和 USING PERIODIC COMMIT 会自行管理事务——仅对这些查询使用 session.run()。executableQuery 和 executeRead/Write 执行这些查询时会失败。
session.run() 重试 [6.1+]:仅针对幂等错误立即重试一次(默认启用)。可针对每个驱动程序或会话禁用:
// Driver-level — disable for all sessions
var config = Config.builder().withAutoCommitRetriesDisabled(true).build();
// Session-level — overrides driver
var sessionConfig = SessionConfig.builder()
.withAutoCommitRetriesMode(AutoCommitRetriesMode.DISABLED) // DEFAULT = follow driver
.build();
executableQuery — 默认选择
// Read — route to replicas
var result = driver.executableQuery("""
MATCH (p:Person {name: $name})-[:KNOWS]->(friend)
RETURN friend.name AS name
""")
.withParameters(Map.of("name", "Alice"))
.withConfig(QueryConfig.builder()
.withDatabase("neo4j") // always specify — avoids home-db round-trip
.withRouting(RoutingControl.READ)
.build())
.execute();
result.records().forEach(r -> System.out.println(r.get("name").asString()));
long ms = result.summary().resultAvailableAfter(TimeUnit.MILLISECONDS);
// Write
driver.executableQuery("CREATE (p:Person {name: $name, age: $age})")
.withParameters(Map.of("name", "Bob", "age", 30))
.withConfig(QueryConfig.builder().withDatabase("neo4j").build())
.execute();
切勿使用字符串插值拼接 Cypher。始终使用 .withParameters(Map.of(...))。
托管事务(executeRead / executeWrite)
会话不具备线程安全性——每个请求/线程使用一个会话,并始终关闭。
try (var session = driver.session(SessionConfig.builder()
.withDatabase("neo4j").build())) {
// Read → replica routing
var names = session.executeRead(tx -> {
var result = tx.run(
"MATCH (p:Person) WHERE p.name STARTS WITH $prefix RETURN p.name AS name",
Map.of("prefix", "Al"));
return result.stream().map(r -> r.get("name").asString()).toList(); // collect INSIDE
});
// Write → leader routing
session.executeWriteWithoutResult(tx ->
tx.run("CREATE (p:Person {name: $name})", Map.of("name", "Carol"))
);
}
必须在回调内部使用完结果
Result 是与打开的事务关联的惰性游标。回调返回时事务会关闭——此后读取会抛出 ResultConsumedException。
// ❌ Returns Result — already closed by the time caller uses it
var result = session.executeRead(tx ->
tx.run("MATCH (p:Person) RETURN p.name AS name"));
result.stream().forEach(...); // throws ResultConsumedException
// ✅ Collect to List inside callback
var names = session.executeRead(tx ->
tx.run("MATCH (p:Person) RETURN p.name AS name")
.stream().map(r -> r.get("name").asString()).toList());
回调规则
- 在下一次调用
tx.run()前使用完每个Result——同时打开多个游标会导致行为未定义。 - 不要产生副作用(HTTP、电子邮件、指标递增)——发生瞬时错误时,回调可能会重试。
- 使用
MERGE(幂等),而不是CREATE,以确保写入可安全重试。 executeRead→ 副本;executeWrite→ 主节点。
TransactionConfig — 超时和元数据
var config = TransactionConfig.builder()
.withTimeout(Duration.ofSeconds(5))
.withMetadata(Map.of("app", "myService", "user", userId)) // visible in SHOW TRANSACTIONS
.build();
session.executeRead(tx -> { /* ... */ }, config);
显式事务
适用于跨越多个方法或需要外部协调的工作。不会自动重试。
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
var tx = session.beginTransaction();
try {
doPartA(tx);
doPartB(tx);
tx.commit();
} catch (Exception e) {
try { tx.rollback(); } catch (Exception rb) { e.addSuppressed(rb); }
throw e;
}
}
tx.rollback() 是一次网络调用——请将其放在单独的 try/catch 中,并使用 addSuppressed,以免丢失原始异常。
提交结果不确定:如果 tx.commit() 抛出 ServiceUnavailableException,提交可能成功,也可能失败。请将写入设计为幂等操作(MERGE + 唯一约束),以便安全重试。
选择显式事务还是托管事务:
- 需要自动重试 →
executeRead/executeWrite - 工作跨越多个方法 → 显式事务(将
tx作为参数传递) - 需要与外部 I/O 协调 → 显式事务(仅在 I/O 成功后提交)
错误处理
try {
driver.executableQuery("...").execute();
} catch (ServiceUnavailableException e) {
// No servers — check connection
} catch (SessionExpiredException e) {
// Server closed session — open new one
} catch (TransientException e) {
// Managed txns retry automatically; explicit txns need manual retry
} catch (Neo4jException e) {
// Cypher/constraint error — e.code() gives GQL status code
}
托管事务会自动重试 TransientException——无需捕获。
数据类型与值提取
| Cypher 类型 | Java 访问器 |
|---|---|
Integer | value.asLong() / value.asInt() |
Float | value.asDouble() |
String | value.asString() |
Boolean | value.asBoolean() |
List | value.asList() |
Map | value.asMap() |
Node | value.asNode() |
Relationship | value.asRelationship() |
Date | value.asLocalDate() |
DateTime | value.asZonedDateTime() |
UUID | value.asUUID() → java.util.UUID [6.2+, Neo4j 2026.08+] |
var record = result.records().get(0);
String name = record.get("name").asString();
long age = record.get("age").asLong();
var node = record.get("p").asNode();
String label = node.labels().iterator().next();
Map<String,Object> props = node.asMap();
空值安全——两种不同的情况
| 情况 | record.get(key) | .asString() |
|---|---|---|
| 键存在,值非空 | 该值 | 返回字符串 |
| 键存在,值为图空值 | .isNull() = true 的 Value | 抛出 Uncoercible |
| 键不存在(拼写错误 / 未投影) | Value.NULL 哨兵值 | 抛出 NoSuchElementException |
// Graph null — use default overload (safe only if key is always projected):
String city = record.get("city").asString("Unknown");
// Absent key — check containsKey first:
if (record.containsKey("city") && !record.get("city").isNull()) {
String city = record.get("city").asString();
}
对象映射
将查询结果直接映射到 Java 记录/类中——无需手动调用访问器。
// Domain record — field names match RETURN aliases (case-sensitive)
public record Person(String name, long age) {}
// Map single record
var person = driver.executableQuery("MATCH (p:Person {name: $name}) RETURN p.name AS name, p.age AS age")
.withParameters(Map.of("name", "Alice"))
.withConfig(QueryConfig.builder().withDatabase("neo4j").build())
.execute()
.records()
.stream()
.map(r -> r.get("name").asString()) // or: r.as(Person.class) — see note
.findFirst()
.orElseThrow();
// Using .as(Person.class) — maps RETURN keys to record fields by name
var person2 = driver.executableQuery("""
MATCH (p:Person {name: $name})
RETURN p.name AS name, p.age AS age
""")
.withParameters(Map.of("name", "Tom Hanks"))
.withConfig(QueryConfig.builder().withDatabase("neo4j").build())
.execute()
.records()
.stream()
.map(record -> record.get("p").as(Person.class))
.findFirst()
.orElseThrow(() -> new RuntimeException("Person not found"));
嵌套映射——返回映射投影,并在列表中加入 COLLECT {}:
public record Movie(String title, List<Person> actors) {}
var movieCypher = """
MATCH (movie:Movie)
LIMIT 1
RETURN movie {
.title,
actors: COLLECT {
MATCH (actor:Person)-[:ACTED_IN]->(movie)
RETURN actor
}
}
""";
var movie = driver.executableQuery(movieCypher)
.withConfig(QueryConfig.builder().withDatabase("neo4j").build())
.execute()
.records()
.stream()
.map(r -> r.get("movie").as(Movie.class))
.findFirst()
.orElseThrow();
只会填充记录中定义的映射属性——Cypher 返回的额外属性会被忽略。
性能模式
始终指定数据库——省略数据库会导致每次调用都往返查询主数据库。
将读取路由到副本——在 QueryConfig 中使用 RoutingControl.READ,或使用 executeRead。
使用 UNWIND 批量写入——传递 List<Map<String,Object>>(仅支持普通 map;自定义对象会失败):
List<Map<String, Object>> rows = people.stream()
.map(p -> Map.<String, Object>of("name", p.name(), "age", p.age()))
.toList();
driver.executableQuery("UNWIND $items AS item MERGE (p:Person {name: item.name}) SET p.age = item.age")
.withParameters(Map.of("items", rows))
.withConfig(QueryConfig.builder().withDatabase("neo4j").build())
.execute();
参数 map 中允许的叶节点类型:String、Long/Integer/Short/Byte、Double/Float、Boolean、List<?>、Map<String,?>、null。自定义对象和 LocalDate 必须先转换。
在一个事务中集中写入——使用一次 executeWrite 并在其中循环,而不是每次迭代都调用一次 executeWrite。
连接池——默认有 100 个连接。连接耗尽时可调整:
Config.builder()
.withMaxConnectionPoolSize(50)
.withConnectionAcquisitionTimeout(30, TimeUnit.SECONDS)
.build()
常见错误
| 错误做法 | 修复方法 |
|---|---|
| 通过字符串插值传入 Cypher 参数 | 始终使用 .withParameters(Map.of(...)) |
| 未指定数据库名称 | 每次都在 QueryConfig / SessionConfig 中设置 |
每次请求都创建新的 Driver | 在启动时创建一次,并在所有地方共享 |
在线程间共享 Session | 每个请求/线程使用一个会话 |
从事务回调返回 Result | 在回调内部收集到 List/Map 中 |
在下一次 tx.run() 前未关闭 Result | 在下一次调用前消费结果 |
| 在托管事务回调中执行副作用 | 移到回调外部——回调可能会重试 |
| 向 UNWIND 参数传递自定义对象 | 转换为 List<Map<String,Object>> |
对图中的 null 调用 asString() | 使用 .asString("default") 或检查 .isNull() |
对不存在的键调用 asString() | 可选访问前先调用 containsKey() |
在 catch 中直接调用 tx.rollback() | 用 try/catch 包装;使用 addSuppressed |
认为 commit() 失败就代表未提交 | 提交状态可能不确定——设计幂等写入 |
在异步回调中阻塞(.join()) | 使用 thenCompose 链接 |
| 异步错误路径中未关闭会话 | 使用 exceptionallyCompose 关闭会话,然后重新抛出异常 |
| 在循环中每次写入都开启一个事务 | 使用 UNWIND 批处理,或在一个回调中分组处理 |
对读取操作使用 executeWrite | 使用 executeRead——将请求路由到副本 |
参考资料
按需加载:
- references/async-reactive.md — 完整的异步
CompletableFuture模式、结合Flux.usingWhen的响应式RxSession、死锁规避 - references/property-encryption.md — 属性加密预览版 [6.3.0+]:信封配置、密钥封装服务(本地、AWS KMS、Azure Key Vault、GCP KMS)、密钥管理器、使用 AAD 加密/解密
- references/advanced-config.md — 完整的
Config.builder()选项、TLS、通知过滤、会话级身份验证、用户模拟、跨会话书签、空间类型(Values.point/WGS-84/Cartesian)
文档:
- Java Driver 手册:https://neo4j.com/docs/java-manual/current/
- API 参考:https://neo4j.com/docs/api/java-driver/current/
检查清单
- 在启动时创建一个
Driver实例;在关闭时将其关闭 - 创建驱动程序后调用
verifyConnectivity() - 在每个
QueryConfig/SessionConfig中指定数据库名称 - 使用参数(绝不通过字符串插值传入 Cypher)
- 在托管事务回调中消费
Result - 在
executeRead/Write回调中不执行副作用 - 使用 try-with-resources 关闭会话
- 在成功和错误路径中都关闭异步会话(
exceptionallyCompose) - 将提交时的
ServiceUnavailableException视为提交状态不确定 -
UNWIND参数使用List<Map<String,Object>>(不使用自定义对象) - 访问可选结果列前先检查
containsKey()