GraphQL LogoGraphQL

全局对象标识

一致的对象访问使简单的缓存和对象查找成为可能

为了为 GraphQL 客户端提供优雅地处理缓存和数据重新获取的选项,GraphQL 服务器需要以标准化的方式公开对象标识符。

为了实现这一点,客户端需要通过标准机制查询以通过 ID 请求对象。然后,在响应中,模式需要提供一种标准方法来提供这些 ID。

由于除了其 ID 之外,对该对象知之甚少,因此我们将这些对象称为“节点”。以下是一个节点的查询示例

{
node(id: "4") {
id
... on User {
name
}
}
}
  • GraphQL 模式被格式化为允许通过根查询对象上的 node 字段获取任何对象。这将返回符合“节点”接口的对象。
  • id 字段可以安全地从响应中提取出来,并可以存储以通过缓存和重新获取进行重复使用。
  • 客户端可以使用接口片段来提取特定于符合节点接口的类型的额外信息。在本例中为“用户”。

节点接口如下所示

# An object with a Globally Unique ID
interface Node {
# The ID of the object.
id: ID!
}

用户通过以下方式符合

type User implements Node {
id: ID!
# Full name
name: String!
}

规范#

以下所有内容都以更正式的要求描述了围绕对象标识的规范,以确保跨服务器实现的一致性。这些规范基于服务器如何与 Relay API 客户端兼容,但对任何客户端都很有用。

保留类型#

与本规范兼容的 GraphQL 服务器必须保留某些类型和类型名称以支持一致的对象标识模型。特别是,本规范为以下类型创建了指南

  • 一个名为Node的接口。
  • 根查询类型上的node字段。

Node 接口#

服务器必须提供一个名为Node的接口。该接口必须包含一个名为id的字段,该字段返回一个非空的ID

id应该是此对象的全局唯一标识符,并且仅凭此id,服务器应该能够重新获取该对象。

内省#

正确实现上述接口的服务器将接受以下内省查询,并返回提供的响应

{
__type(name: "Node") {
name
kind
fields {
name
type {
kind
ofType {
name
kind
}
}
}
}
}

产生

{
"__type": {
"name": "Node",
"kind": "INTERFACE",
"fields": [
{
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {
"name": "ID",
"kind": "SCALAR"
}
}
}
]
}
}

Node 根字段#

服务器必须提供一个名为node的根字段,该字段返回Node接口。此根字段必须接受一个参数,一个名为id的非空ID。

如果查询返回一个实现了Node的对象,那么当服务器在Nodeid字段中返回的值作为id参数传递给node根字段时,此根字段应该重新获取相同对象。

服务器必须尽力获取此数据,但并非总是可行;例如,服务器可能会返回一个具有有效idUser,但当请求使用node根字段重新获取该用户时,用户的数据库可能不可用,或者用户可能已删除其帐户。在这种情况下,查询此字段的结果应该是null

内省#

正确实现上述要求的服务器将接受以下内省查询,并返回包含提供的响应的响应。

{
__schema {
queryType {
fields {
name
type {
name
kind
}
args {
name
type {
kind
ofType {
name
kind
}
}
}
}
}
}
}

产生

{
"__schema": {
"queryType": {
"fields": [
// This array may have other entries
{
"name": "node",
"type": {
"name": "Node",
"kind": "INTERFACE"
},
"args": [
{
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {
"name": "ID",
"kind": "SCALAR"
}
}
}
]
}
]
}
}
}

字段稳定性#

如果两个对象出现在一个查询中,并且都实现了具有相同 ID 的Node,那么这两个对象必须相等。

出于此定义的目的,对象相等定义如下

  • 如果在两个对象上查询一个字段,则在第一个对象上查询该字段的结果必须等于在第二个对象上查询该字段的结果。
    • 如果字段返回一个标量,则相等定义为适合该标量的定义。
    • 如果字段返回一个枚举,则相等定义为两个字段都返回相同的枚举值。
    • 如果字段返回一个对象,则相等定义为根据上述递归定义。

例如

{
fourNode: node(id: "4") {
id
... on User {
name
userWithIdOneGreater {
id
name
}
}
}
fiveNode: node(id: "5") {
id
... on User {
name
userWithIdOneLess {
id
name
}
}
}
}

可能会返回

{
"fourNode": {
"id": "4",
"name": "Mark Zuckerberg",
"userWithIdOneGreater": {
"id": "5",
"name": "Chris Hughes"
}
},
"fiveNode": {
"id": "5",
"name": "Chris Hughes",
"userWithIdOneLess": {
"id": "4",
"name": "Mark Zuckerberg"
}
}
}

因为fourNode.idfiveNode.userWithIdOneLess.id相同,根据上述条件,我们保证fourNode.name必须与fiveNode.userWithIdOneLess.name相同,实际上也是如此。

复数标识根字段#

想象一个名为username的根字段,它接受用户的用户名并返回相应的用户

{
username(username: "zuck") {
id
}
}

可能会返回

{
"username": {
"id": "4"
}
}

显然,我们可以将响应中的对象(ID 为 4 的用户)与请求链接起来,通过用户名“zuck”标识对象。现在想象一个名为usernames的根字段,它接受用户名列表并返回对象列表

{
usernames(usernames: ["zuck", "moskov"]) {
id
}
}

可能会返回

{
"usernames": [
{
"id": "4"
},
{
"id": "6"
}
]
}

为了让客户端能够将用户名与响应链接起来,它需要知道响应中的数组大小与作为参数传递的数组大小相同,并且响应中的顺序与参数中的顺序匹配。我们称这些为复数标识根字段,它们的具体要求如下。

字段#

符合此规范的服务器可能会公开接受输入参数列表并返回响应列表的根字段。为了让符合规范的客户端使用这些字段,这些字段必须是复数标识根字段,并遵守以下要求。

注意 符合规范的服务器可能会公开不是复数标识根字段的根字段;符合规范的客户端将无法在查询中将这些字段用作根字段。

复数标识根字段必须有一个参数。该参数的类型必须是非空列表,其中包含非空元素。在我们的usernames示例中,该字段将接受一个名为usernames的参数,其类型(使用我们的类型系统简写)将是[String!]!

复数标识根字段的返回类型必须是列表,或者是非空包装器,其中包含列表。该列表必须包含Node接口、实现Node接口的对象或这些类型的非空包装器。

无论何时使用复数标识根字段,响应中列表的长度必须与参数中列表的长度相同。响应中的每个项目必须与其在输入中的项目相对应;更正式地说,如果将输入列表Lin传递给根字段导致输出值为Lout,那么对于任意排列P,将P(Lin)传递给根字段必须导致输出值为P(Lout)

因此,建议服务器不要让响应类型包装非空包装器,因为如果它无法为输入中的给定条目获取对象,它仍然必须为该输入条目提供输出中的值;null是执行此操作的有用值。

继续阅读 →缓存