对象映射

简介

有许多不同的工具可以使用 JDBC 进行对象‑关系映射(ORM、O/RM 和 O/R 映射工具),Neo4j JDBC 驱动支持其中的一部分,例如 MyBatis,或使用 JDBI、Spring JDBC Template 等工具进行基于结果集/元组的映射。然而,这些工具常常会生成 SQL 查询,并且往往依赖非常特定的 SQL 功能,而这些功能我们无法在 SQL 到 Cypher® 翻译 中完全实现,因此实际效果可能因情况而异。

在 Neo4j 中进行对象映射的最“图形化”方式是使用 Neo4j-OGM(提供对 Spring 和 Quarkus 的集成),或 Spring Data Neo4j。两者均基于 通用 Neo4j Java Driver 构建。如果您需要一个包括仓库支持、先进的映射和查询能力的端到端解决方案,这两种方案都是首选。

有时,简单的方案已经足够,而一种已经可用的解决方案是将图数据直接映射为 JSON 对象并在查询中传回 JSON 对象。JDBC 规范允许通过 ResultSetPreparedStatement 类型获取和设置任意类型的对象,Neo4j JDBC 驱动正是利用这些特性将节点和关系转换为 JSON 对象。

由于 JDK 中没有标准的 JSON 对象,此功能需要额外的可选依赖,具体如下面章节所述。

使用 Jackson Databind

Neo4j JDBC 驱动可以使用 Jackson Databind 将图对象转换为 JSON 节点,并将这些对象读取回可在 Cypher 查询中使用的映射。

在您的类路径或模块路径中加入以下依赖,以启用映射到 JsonNode 类型的对象

示例 1. 通过 Jackson Databind 进行对象映射所需的依赖
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.19.1</version>
</dependency>

现在,您可以将 JsonObject.class 作为类型参数传递给 ResultSet 的任意 getObject 重载(该重载支持类型参数),以如此方式检索 JSON

示例 2. 将节点列表检索为 JSON 数组
import java.io.StringWriter;
import java.sql.DriverManager;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public class ReadNodesIntoJson {

    public static void main(String... args) throws Exception {

        var objectMapper = new ObjectMapper();
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
        objectMapper.enable(JsonGenerator.Feature.IGNORE_UNKNOWN);

        try (var connection = DriverManager.getConnection("jdbc:neo4j://:7687/movies", "neo4j", "verysecret");
                var stmt = connection.createStatement()) {

            var result = stmt.executeQuery("""
                    MATCH (n:Movie) LIMIT 2
                    WITH n RETURN collect(n) AS movies
                    """);
            result.next();

            var json = result.getObject("movies", JsonNode.class);
            var sw = new StringWriter();
            objectMapper.writeTree(objectMapper.createGenerator(sw), json);
            System.out.println(sw);
        }
    }
}

输出将类似于如下内容

[ {
  "elementId" : "4:5c0c7e77-4034-45a1-ab00-a159be8dbf04:0",
  "labels" : [ "Movie" ],
  "properties" : {
    "title" : "The Matrix",
    "tagline" : "Welcome to the Real World",
    "released" : 1999
  }
}, {
  "elementId" : "4:5c0c7e77-4034-45a1-ab00-a159be8dbf04:9",
  "labels" : [ "Movie" ],
  "properties" : {
    "title" : "The Matrix Reloaded",
    "tagline" : "Free your mind",
    "released" : 2003
  }
} ]

您会看到节点包含其 element id、标签列表以及属性对象。此结构与 Query API 对齐,因此任何映射都可以直接使用。JDBC 驱动始终采用“Plain JSON”格式,这样后续映射到领域对象时尽可能简单,无需自定义反序列化器。

下面是一个示例,展示了一个查询将匹配到的所有电影及其演员的结果组织为映射(map),收集为列表,再一次检索为 JSON 节点,最终可以通过 Jackson 的 ObjectMapper 映射为领域对象列表

示例 3. 将 JSON 节点映射为领域对象
import java.sql.DriverManager;
import java.util.List;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class MapNodesIntoObjects {

    public static void main(String... args) throws Exception {

        var objectMapper = new ObjectMapper();

        record Actor(String name, short born) {
        }
        record Movie(String title, short released, List<Actor> actors) {
        }

        try (var connection = DriverManager.getConnection("jdbc:neo4j://:7687/movies", "neo4j",
                "verysecret"); var stmt = connection.createStatement()) {

            var result = stmt.executeQuery("""
                    MATCH (m:Movie)<-[:ACTED_IN]-(a:Person)
                    WITH m, collect(a{.*}) AS actors
                    ORDER BY m.title
                    LIMIT 5
                    RETURN collect({title: m.title, released: m.released, actors: actors})
                    """);
            result.next();

            var json = result.getObject(1, JsonNode.class); (1)
            var movies = objectMapper.treeToValue(json, new TypeReference<List<Movie>>() {}); (2)

            movies.forEach(System.out::println);
        }
    }
}
1 首先再次将列表检索为 JSON 数组
2 使用 Jackson 的 ObjectMapper 将该数组映射为包含演员的 Movie 对象列表

当然,写回 JSON 节点同样可行

示例 4. 将 JSON 节点用作参数
import java.sql.DriverManager;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class WritingObjects {

    public static void main(String... args) throws Exception {

        var objectMapper = new ObjectMapper();

        record Movie(String title, String tagline, long released) {
        }

        var movie = new Movie("title", "tagline", 2025);
        try (var connection = DriverManager.getConnection("jdbc:neo4j://:7687/movies", "neo4j",
                "verysecret"); var stmt = connection.prepareStatement("CREATE (m:Movie $1) RETURN m")) {
            stmt.setObject(1, objectMapper.valueToTree(movie));
            var rs = stmt.executeQuery();
            rs.next();
            var json = rs.getObject("m", JsonNode.class);
            var newMovie = objectMapper.treeToValue(json.get("properties"), Movie.class);
            System.out.println("New movie " + newMovie + " has id " + json.get("elementId"));
        }
    }
}

它将产生类似如下的输出

New movie Movie[title=title, tagline=tagline, released=2025] has id "4:5c0c7e77-4034-45a1-ab00-a159be8dbf04:173"