将查询结果映射为对象

处理查询结果返回的值 时,需要手动提取它们的属性并转换为相应的 Java 类型。例如,要将 name 属性检索为字符串,需要使用 person.get("name").asString()

借助驱动的 Value Mapping 功能,你可以声明一个 Java Record,描述查询预期返回的值的规格,并让驱动使用该类根据查询结果生成新对象。

将驱动值映射到本地类

要将记录映射为对象,请定义一个 Java Record,其组件需与查询返回的键完全一致。构造函数参数必须精确匹配查询返回键,且区分大小写。

最直接的做法是使用 Java Record,但使用一个普通类,只要其构造函数与查询返回键相匹配,也同样可行。无论哪种方式,都需要通过 Value.as() 方法把类定义提供给驱动。

:Person 节点映射到 Person 记录对象
package demo;

import java.util.Map;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.QueryConfig;

public class App {

    private static final String dbUri = "<database-uri>";
    private static final String dbUser = "<username>";
    private static final String dbPassword = "<password>";

    public static void main(String... args) {
        try (var driver = GraphDatabase.driver(dbUri, AuthTokens.basic(dbUser, dbPassword))) {
            record Person(String name, Integer age) {}
            var persons = driver.executableQuery("MERGE (p:Person {name: $name, age: $age}) RETURN p")
                .withParameters(Map.of("name", "Margarida", "age", 29))
                .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
                .execute()
                .records()
                .stream()
                .map(record -> record.get("p").as(Person.class))
                .toList();
            System.out.println(persons.get(0));  // Person[name=Margarida, age=29]
        }
    }
}

没有匹配属性的参数会得到 null 值。如果参数不接受 null(例如基本类型),则必须提供一个 可替代的构造函数,该构造函数不包含该参数。上例使用 Integer 而不是基本类型 int,以处理缺少 age 属性的节点。

在查询语句旁直接声明 record 对象是一种便捷的方式,能够快速获取易于提取属性的结果。但由于该类定义在局部作用域,其类型无法在方法之外引用。因此,此类不能作为方法的返回类型使用:要么在同一函数中处理映射后的值,要么确保它实现了在方法外部可访问的接口或父类。另一种做法是将 record 声明为类的 public 成员,或创建一个独立的类来包含你的 record 定义,这样映射后的对象即可在方法作用域之外使用。

虽然支持使用特定的复杂类型作为构造函数参数(例如 record Friends(List<String> names) {}),但不支持使用泛型复杂类型的构造函数参数(例如 record Friends<T>(List<T> names) {}

将属性映射为不同的名称 (@Property)

记录的属性名称与查询返回键可以不同。例如,考虑节点 (:Person {name: "Alice"})。查询 MERGE (p:Person {name: "Alice"}) RETURN p.name 返回的键是 p.name,即使属性名本身是 name。同理,查询 MERGE (pers:Person {name: "Alice"}) RETURN pers.name 的返回键为 pers.name

你可以随时使用 Cypher 运算符 AS 修改返回键(例如 MERGE (p:Person {name: "Alice"}) RETURN p.name AS name),或者使用 @Property(<dbPropertyName>) 注解来指定后续构造函数参数应映射到的属性名。

将属性 name/age 映射到对象属性 firstName/Years
// import org.neo4j.driver.mapping.Property;

record Person(@Property("name") String firstName, @Property("age") Integer Years) {}
var persons = driver.executableQuery("MERGE (p:Person {name: $name, age: $age}) RETURN p")
    .withParameters(Map.of("name", "Margarida", "age", 29))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute()
    .records()
    .stream()
    .map(record -> record.get("p").as(Person.class))
    .toList();
System.out.println(persons.get(0));  // Person[firstName=Margarida, Years=29]

将驱动记录映射到本地类

前面的示例将驱动的 Value(例如标识为 p 的节点)映射到某个类。你同样可以使用映射功能对驱动的 Record 实例进行映射,方法是调用 Record.as() 方法。

返回键 name/p.age 映射到对象属性 Name/Age
// import org.neo4j.driver.mapping.Property;

record Person(String name, @Property("p.age") Integer age) {}
var persons = driver.executableQuery("""
            MERGE (p:Person {name: $name, age: $age})
            RETURN p.name AS name, p.age
        """)
    .withParameters(Map.of("name", "Margarida", "age", 29))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute()
    .records()
    .stream()
    .map(record -> record.as(Person.class))
    .toList();
System.out.println(persons.get(0));  // Person[name=Margarida, age=29]

使用多个构造函数

你的 Java record 类可以包含多个构造函数。在这种情况下,驱动会依据以下规则(按优先级顺序)挑选合适的构造函数:

  • 匹配属性最多

  • 不匹配属性最少

构造函数必须至少有一个属性匹配,才能用于映射。

用于处理可选 age 属性的额外构造函数
// import org.neo4j.driver.mapping.Property;

record Person(String name, int age) {
    public Person(@Property("name") String name) {
        this(name, -1);
    }
}
var persons = driver.executableQuery("MERGE (p:Person {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Axel"))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute()
    .records()
    .stream()
    .map(record -> record.get("p").as(Person.class))
    .toList();

除非使用编译器 -parameters 选项,或参数属于 java.lang.Record 的规范构造函数,否则编译器默认会重命名构造函数参数。

在上面的示例中,仅包含 name 的构造函数仍使用了 @Property 注解,即使它并未指定与构造函数参数不同的名称。这是因为该构造函数并非规范构造函数,必须显式标注。

插入和更新数据

你也可以利用映射功能来插入或更新数据:先创建一个 Java Record 实例作为对象的蓝图,然后将其作为参数传递给查询。

创建并更新 :Person 节点
record Person(String name, int age) {}

var person = new Person("Lucia", 29);
driver.executableQuery("CREATE (:Person $person)")
         .withParameters(Map.of("person", person))
         .execute();

var happyBirthday = new Person("Lucia", 30);
driver.executableQuery("""
            MATCH (p:Person {name: $person.name})
            SET p += $person
            """)
    .withParameters(Map.of("person", happyBirthday))
    .execute();