将查询结果映射为对象

从数据库读取数据时,结果将以 IRecord 对象的形式返回。要处理这些记录,您可以利用 .Get() 及其他辅助方法提取属性,也可以将它们映射为应用程序中的对象。您可以将记录映射为您明确定义的类,或者提供一个记录特定的蓝图(blueprint),驱动程序将据此生成新的对象。

对象映射功能可以帮助您减少样板代码,并更轻松地处理查询结果。

要使用对象映射功能,您需要在根命名空间 Neo4j.Driver 旁边引入 Neo4j.Driver.Mapping 命名空间。

映射到现有类

如果您想将记录映射到现有的类,请定义一个与查询返回键具有相同属性的类。类属性必须与查询返回的键完全匹配,且区分大小写。

要将记录映射到名为 className 的类,请使用方法 IRecord.AsObject<className>()

:Person 节点映射到 Person
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

var record = result.Result[0];
var person = record.AsObject<Person>();  (2)
Console.WriteLine(person.name);  // "Alice"

class Person {  (3)
    public string name { get; set; }  (4)
    public int age { get; set; }
}
1 在查询中使用别名来匹配类属性。
2 该记录被映射为 Person 类的一个实例。
3 Person 类包含 nameage 属性,正如查询返回的键一样。
4 每个类属性必须具有公共的 getter setter,以便驱动程序在实例化类后进行调用。如果这不符合您的用例,请参阅如何使用构造函数并使用私有 setter。
记录的属性名称与其查询返回键可能不同。

例如,考虑一个标签为 :Person 且具有 name 属性的节点。
对于查询 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)。否则,您可以使用类属性修饰符 [MappingSource("returnedKeyName")] 来更改属性名称与类属性之间的关联。有关详细信息,请参阅所有修饰符

p.name 映射到 name,将 p.age 映射到 age
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

var person = result.Result[0].AsObject<Person>();
Console.WriteLine(person.name);  // "Alice"

class Person {  (2)
    [MappingSource("p.name")] (3)
    public string name { get; set; }

    [MappingSource("p.age")]
    public int age { get; set; }
}
1 查询返回该节点,从而得到记录键 p.namep.age
2 Person 类拥有 nameage 属性,即使查询中不存在这样的返回键。
3 MappingSource 修饰符将记录键 p.name 与类属性 name 关联起来。

类属性修饰符

为了使映射更加灵活,可以使用修饰符。在类属性上使用它们可以更改每个属性映射到其对应查询返回键的方式。

  • [MappingSource("<returnedKeyName>")] —— 将 returnedKeyName 映射到类属性,而不是查找与类属性名称匹配的返回键。

  • [MappingIgnored] —— 忽略该类属性(即不查找对应的返回键)。

  • [MappingOptional] —— 如果能匹配到该类属性则使用,如果查询没有返回可匹配的键,则忽略它。

  • [MappingDefaultValue(val)] —— 与 [MappingOptional] 相同,并在缺少匹配项时指定默认值 val

修饰符示例
class Person {
    [MappingSource("p.name")]  (1)
    public string name { get; set; }

    [MappingOptional]  (2)
    public int age { get; set; }

    [MappingIgnored]  (3)
    public string address { get; set; }

    [MappingDefaultValue("Subscriber")]  (4)
    public string role { get; set; }
}
1 p.name 返回键映射到 name 类属性。
2 如果存在 age 返回键,则填充 age 类属性,否则将该属性设为 null
3 无论查询返回键如何,均将 address 类属性设置为 null
4 如果存在 role 返回键,则填充 role 类属性,否则将该属性设为默认值 Subscriber

[MappingSource] 修饰符的一个额外可选参数允许您将节点标签和关系类型映射到类属性中。

  • [MappingSource("nodeEntity", EntityMappingSource.NodeLabel)] —— 将别名 nodeEntity 下返回的节点中找到的标签收集到给定的类属性中(作为字符串列表)。MappingSource 的第一个参数必须是引用节点实体的返回键。如果属性类型不是 List<string> 而是普通 string,您将获得逗号分隔的标签列表。

  • [MappingSource("relEntity", EntityMappingSource.RelationshipType)] —— 将 job 关系类型映射到给定的类属性。[MappingSource] 的第一个参数必须是引用关系实体的返回键。

映射节点标签和关系类型
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person:Actor {name: $name})
    MERGE (m:Movie {name: $movie})
    MERGE (p)-[r:ACTED_IN {role: $role}]->(m)
    RETURN p AS person, r AS role  (1)
    ")
    .WithParameters(new { name = "Keanu Reeves", movie = "The Matrix", role = "Neo" })
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

var person = result.Result[0].AsObject<Person>();
Console.WriteLine(string.Join(",", person.Labels));  // "Person,Actor"
Console.WriteLine(person.Role);  // "Neo"

class Person {
    [MappingSource("person", EntityMappingSource.NodeLabel)]  (2)
    public List<string> Labels { get; set; }

    [MappingSource("role", EntityMappingSource.RelationshipType)]  (3)
    public string Role { get; set; }
}
1 person 是一个节点实体;role 是一个关系实体。
2 Labels 收集在 person 节点中找到的标签。
3 Role 包含 role 关系的类型。

使用构造函数

将记录映射到现有类时,驱动程序首先要做的是实例化一个对象以进行映射,这将导致类构造函数被调用。

  • 如果未定义显式构造函数,驱动程序将选择隐式定义的无参构造函数,然后继续通过 setter 设置类属性。

  • 如果定义了一个或多个构造函数,驱动程序将选择最简单的一个(即参数最少的一个)。您可以通过在希望驱动程序使用的构造函数上添加 [MappingConstructor] 修饰符来覆盖此选择。

定义构造函数对于使结果对象不可变非常有用:构造函数为驱动程序提供了一种设置部分(或全部)属性的方法,但其他任何组件都无法更改这些属性。调用构造函数后,驱动程序会继续为具有公共 setter 的属性设置值(如果有)。

您可以在构造函数参数中使用修饰符,尽管只有 [MappingOptional][MappingDefaultValue] 有意义(其他任何需求最好通过更改构造函数签名来实现)。

一个具有两个构造函数和私有 setter 的类
class Person {
    [MappingConstructor]  (1)
    public Person(string name, [MappingDefaultValue(21)] int age) {
        Name = name;
        Age = age;
    }

    public Person() {  (2)
    }

    public string Name { get; private set; }  (3)
    public int Age { get; private set; }
}
1 该修饰符指示驱动程序在将记录映射到 Person 类时将其用作构造函数。构造函数签名暗示 name 返回键映射到 Name 类属性,age 映射到 Age(默认值为 21)。
2 如果 [MappingConstructor] 修饰符未出现在另一个构造函数中,则会调用此形式的构造函数。
3 因为构造函数设置了 NameAge 属性,所以这些属性可以使用私有 setter。

映射到匿名类型

特别是当您的查询返回多种结果键时,仅仅为了映射而定义许多不同的类,或者设计一个单一类并通过属性修饰符使其适应各种返回键,可能会显得没有意义。在这种情况下,您可以向驱动程序提供一个定义骨架(蓝图),并让它据此实例化匿名对象。

您可以使用 IRecord.AsObjectFromBlueprint() 方法提供蓝图,且记录必须与其完全匹配(即您不能使用修饰符)。

var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

var person = result.Result[0].AsObjectFromBlueprint(new { name = "", age = 0 });  (2)
1 返回键必须使用别名,因为 .AsObjectFromBlueprint() 不支持标识符中的点号。
2 键名必须与返回键匹配。提供的初始值不作为默认值使用;它们仅用于推断属性类型(即定义中提供的具体值会被丢弃)。

如果您想避免在返回键中使用别名,可以在 .AsObjectFromBlueprint() 调用中嵌套对象。

var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

var person = result.Result[0].AsObjectFromBlueprint(new { p = new { name = "", age = 0 }});  (2)
1 唯一的返回键是 p,其中包含 p.namep.age
2 嵌套对象蓝图处理节点实体返回键。

映射到匿名类型的另一种方法是将 .AsObject() 与 Lambda 函数配合使用。这还允许您添加与数据库属性不匹配的属性(如下例中的 BirthYear)。

var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

var person = result.Result[0].AsObject( (string name, int age) => new { Name = name, Age = age, BirthYear = 2025 - age });

Lambda 函数不必创建映射:它可以执行任何处理。

返回字符串的 Lambda
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

var text = result.Result[0].AsObject( (string name, int age) => $"{name} is {age} years old." );

术语表

LTS (长期支持版)

长期支持 (Long Term Support) 版本是保证在若干年内得到支持的版本。Neo4j 4.4 和 5.26 是 LTS 版本。

Aura

Aura 是 Neo4j 的全托管云服务。它提供免费和付费计划。

Cypher

Cypher 是 Neo4j 的图查询语言,允许您从数据库中检索数据。它就像 SQL,但专用于图数据库。

APOC

Awesome Procedures On Cypher (APOC) 是一个包含(许多)函数的库,这些函数在 Cypher 本身中难以轻松实现。

Bolt

Bolt 是用于 Neo4j 实例和驱动程序之间交互的协议。默认监听 7687 端口。

ACID

原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation)、持久性 (Durability) (ACID) 是保证数据库事务可靠处理的属性。符合 ACID 的 DBMS 确保即使发生故障,数据库中的数据也能保持准确和一致。

最终一致性

如果一个数据库能保证所有集群成员在某个时间点都存储了数据的最新版本,则该数据库具有最终一致性。

因果一致性

如果读写查询被集群中的每个成员以相同的顺序看到,则数据库具有因果一致性。这比最终一致性更强。

NULL

空标记不是一种类型,而是缺失值的占位符。更多信息,请参阅 Cypher → 使用 null

事务

事务是一个工作单元,要么被提交,要么在失败时被回滚。例如银行转账:它涉及多个步骤,但它们必须全部成功或全部撤销,以避免钱从一个账户扣除却未存入另一个账户的情况。

背压

背压是对数据流的抵抗力。它确保客户端不会被过快发送的数据压垮,从而超出其处理能力。

书签

书签是代表数据库某种状态的标记。通过将一个或多个书签与查询一起传递,服务器将确保在所表示的状态建立之前,该查询不会被执行。

事务函数

事务函数是由 .ExecuteReadAsync().ExecuteWriteAsync() 调用执行的回调。如果服务器发生故障,驱动程序会自动重新执行回调。

IDriver

IDriver 对象保存了与 Neo4j 数据库建立连接所需的详细信息。