Java集成与开发
前面章节我们已经学习了Neo4j的基础操作,并在Neo4j Browser中进行了大量实验。这篇笔记我们将介绍具体如何在Java程序中连接Neo4j、执行查询、管理事务、处理参数化查询以及将查询结果映射为Java对象。
我们这里将使用Neo4j 5.26.0的驱动包进行演示,如果你在Spring工程中使用,可能还需要参考SpringData Neo4j相关章节,本篇笔记不会涉及Spring相关内容。
引入Maven依赖
首先我们需要在Java工程中引入Neo4j的驱动包,驱动包的版本与数据库保持一致即可。
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>5.26.0</version>
</dependency>
驱动包还需要日志相关依赖SLF4J,以及一个日志实现框架,我们这里使用Logback。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
连接并操作Neo4j数据库
创建Driver实例
Driver是连接Neo4j的客户端对象,它本质上是一个封装了Neo4j Bolt协议的客户端。Driver应是整个应用生命周期内的全局单例对象,它内部持有连接池,不要每次请求都创建和销毁。我们这里将Driver的管理封装为Neo4jClientManager类。
package com.gacfox.demo;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.neo4j.driver.Config;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import java.util.concurrent.TimeUnit;
@Slf4j
public class Neo4jClientManager {
@Getter
private static final Driver driver = GraphDatabase.driver(
"bolt://localhost:7687",
Config.builder()
.withMaxConnectionPoolSize(50)
.withConnectionAcquisitionTimeout(30, TimeUnit.SECONDS)
.withMaxConnectionLifetime(1, TimeUnit.HOURS)
.withConnectionLivenessCheckTimeout(60, TimeUnit.SECONDS)
.build()
);
static {
driver.verifyConnectivity();
log.info("连接neo4j成功");
}
public static void close() {
driver.close();
}
}
其中driver.verifyConnectivity()方法可以验证客户端是否能够成功连接Neo4j。
开启Session
Session是执行查询和事务的基本单元,它从连接池中借用一条连接,用完后归还。Session不是线程安全的,不能在多个线程间共享,Session应当在方法内部创建、使用完毕后关闭。Session实现了AutoCloseable,因此推荐使用try-with-resources写法。
try (Session session = driver.session(SessionConfig.forDatabase("neo4j"))) {
// 操作数据库
}
其中,forDatabase()的参数是数据库名。
执行查询
session.executeRead()用于执行查询,executeRead()接受一个Lambda函数,参数tx是一个ManagedTransaction对象,它代表一个由Driver自动管理的事务,我们的操作会在事务中执行,Lambda正常返回时事务自动提交,抛出异常时自动回滚。不过这里由于是只读的,因此也没什么可回滚的。
package com.gacfox.demo;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;
import org.neo4j.driver.SessionConfig;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Driver driver = Neo4jClientManager.getDriver();
try (Session session = driver.session(SessionConfig.forDatabase("neo4j"))) {
String name = session.executeRead(tx -> {
Result result = tx.run(
"MATCH (p:Person {name: $name}) RETURN p.name AS name",
Map.of("name", "Alice")
);
return result.single().get("name").asString();
});
System.out.println("查询结果:" + name);
}
}
}
此外要注意的是,我们这里的写法其实是参数化查询。和SQL注入问题一样,直接将用户输入拼接进Cypher字符串是极其危险的写法,恶意构造的输入会破坏查询逻辑。使用参数化查询的另一个优势是Neo4j会对Cypher语句进行编译和缓存,参数化的语句结构固定,可以重复命中缓存;而每次拼接不同值的字符串,会导致每次都重新编译。
参数化查询的参数使用Map传递,Java 9+版本的Map.of()写起来很方便,但如果你用的是较低版本的JDK也没关系,Neo4j驱动包中还有一个Values.parameters()方法,写法也是一样的。
Values.parameters("name", "Bob", "age", 25)
参数类型
Driver支持将以下Java类型直接作为参数传入。
| Java 类型 | Neo4j 类型 |
|---|---|
String |
String |
Integer / Long |
Integer |
Double / Float |
Float |
Boolean |
Boolean |
List<?> |
List |
Map<String, ?> |
Map |
null |
Null |
日期时间类型包括LocalDate、LocalDateTime、ZonedDateTime等也原生支持,Driver会自动映射到Neo4j的时间类型。
import java.time.LocalDate;
tx.run(
"CREATE (e:Event {name: $name, date: $date})",
Values.parameters("name", "Launch", "date", LocalDate.of(2025, 6, 1))
);
获取查询结果
tx.run()的返回值是Result对象,它是一个懒加载的游标,只有在遍历时才真正从网络读取数据。Result中的每行对应一个Record,Record中每个字段可以通过列名或列索引访问,返回值类型是Value。
Result result = tx.run("MATCH (p:Person) RETURN p.name AS name, p.age AS age");
while (result.hasNext()) {
Record record = result.next();
String name = record.get("name").asString();
int age = record.get("age").asInt();
System.out.println(name + ", " + age);
}
Value提供了一系列asXxx()方法用于类型转换,常用的如下。
| 方法 | 返回类型 |
|---|---|
asString() |
String |
asInt() |
int |
asLong() |
long |
asDouble() |
double |
asBoolean() |
boolean |
asList() |
List<Object> |
asMap() |
Map<String, Object> |
asNode() |
Node |
asLocalDate() |
LocalDate |
isNull() |
boolean |
如果字段值可能为null,我们应先调用isNull()判断,否则直接转换会抛出异常,或者也可以使用带默认值的重载形式。下面例子中,如果email字段为null返回空字符串。
String email = record.get("email").asString("");
如果确定查询只返回一条记录,也可以使用result.single()直接取出,但如果结果不是恰好一条(零条或多条),它会抛出异常。
Result result = tx.run("MATCH (p:Person {id: $id}) RETURN p.name AS name",
Map.of("id", 1001));
Record record = result.single();
String name = record.get("name").asString();
对于可能不存在或有多条的情况,我们也可以用result.list()取出全部结果。
List<Record> records = tx.run("MATCH (p:Person {id: $id}) RETURN p",
Map.of("id", 1001)).list();
if (records.isEmpty()) {
System.out.println("未找到该用户");
} else {
// 处理结果
}
访问节点属性
当Cypher查询返回的是整个节点而不是具体字段时,我们可以通过Value.asNode()拿到Node对象,再从中读取属性和标签。
Result result = tx.run("MATCH (p:Person {id: $id}) RETURN p", Map.of("id", 1001));
if (result.hasNext()) {
Node node = result.next().get("p").asNode();
// 读取标签
node.labels().forEach(System.out::println);
// 读取属性
String name = node.get("name").asString();
int age = node.get("age").asInt();
}
类似地,关系对象可以通过Value.asRelationship()取出,它具有type()、startNodeElementId()、endNodeElementId()和相关属性访问能力。
执行写操作
session.executeWrite()用于执行写操作。
try (Session session = driver.session(SessionConfig.forDatabase("neo4j"))) {
session.executeWrite(tx -> {
tx.run(
"CREATE (p:Person {id: $id, name: $name, age: $age})",
Map.of("id", 1001, "name", "Alice", "age", 30)
);
return null;
});
}
事务管理
自动事务 vs 手动事务
前面我们用的executeRead()和executeWrite()是Driver提供的自动托管事务,它帮我们处理了提交和回滚,但有些场景下我们需要对事务有更细粒度的控制,比如在同一个事务内执行多条Cypher并在中间穿插Java业务逻辑,此时可以使用手动事务。
try (Session session = driver.session(SessionConfig.forDatabase("neo4j"))) {
try (Transaction tx = session.beginTransaction()) {
try {
tx.run("MATCH (p:Person {id: $id}) SET p.status = 'processing'",
Map.of("id", 1001));
// 假设这里是某些Java逻辑
boolean success = someBusinessLogic();
if (success) {
tx.run("MATCH (p:Person {id: $id}) SET p.status = 'done'",
Map.of("id", 1001));
tx.commit();
} else {
tx.rollback();
}
} catch (Exception e) {
tx.rollback();
throw e;
}
}
}
手动事务中,Transaction也实现了AutoCloseable,在try-with-resources块结束时,如果事务没有显式commit(),Driver会自动调用rollback(),这是一个安全的兜底行为,但我们最好还是明确写出提交或回滚逻辑,这样代码更加直观。
Neo4j事务使用注意事项
对于Neo4j事务,我们有几点要特别注意:
避免长事务:Neo4j中长事务会持有锁,这会影响并发性能,我们应尽量将事务范围控制在必要的Cypher操作上,避免在事务内部做耗时的IO操作。
线程安全问题:Session和Transaction都不是线程安全的,每个线程应独立创建自己的Session。
重试:在executeRead()和executeWrite()中,Driver遇到瞬时错误(如主从切换、临时网络抖动)时会自动重试Lambda函数内逻辑,因此这些逻辑必须是幂等的,不能有“只能执行一次”的副作用。