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

日期时间类型包括LocalDateLocalDateTimeZonedDateTime等也原生支持,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中的每行对应一个RecordRecord中每个字段可以通过列名或列索引访问,返回值类型是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操作。

线程安全问题SessionTransaction都不是线程安全的,每个线程应独立创建自己的Session

重试:在executeRead()executeWrite()中,Driver遇到瞬时错误(如主从切换、临时网络抖动)时会自动重试Lambda函数内逻辑,因此这些逻辑必须是幂等的,不能有“只能执行一次”的副作用。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。