全局对象标识

一致的对象访问机制可实现简单的缓存和对象查找

为了让 GraphQL 客户端能够优雅地处理缓存和数据重新获取,GraphQL 服务端需要以标准化的方式暴露对象标识符。

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

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

{
  node(id: "4") {
    id
    ... on User {
      name
    }
  }
}
  • GraphQL Schema 的格式允许通过根查询对象上的 node 字段获取任何对象。这将返回符合“Node”接口的对象。
  • id 字段可以安全地从响应中提取,并可以存储以便在缓存和重新获取时重复使用。
  • 客户端可以使用接口片段(interface fragments)来提取符合 Node 接口的特定类型的附加信息。在本例中是一个“User”。

Node 接口如下所示

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

User 的实现方式为

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

规范

下文以更正式的要求描述了关于对象标识的规范,以确保不同服务端实现之间的一致性。这些规范基于服务端如何兼容 Relay API 客户端,但对任何客户端都很有用。

保留类型

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

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

Node 接口

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

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

内省(Introspection)

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

{
  __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"
                }
              }
            }
          ]
        }
      ]
    }
  }
}

字段稳定性

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

就此定义而言,对象相等性定义如下

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

例如

{
  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"
    }
  ]
}

为了让客户端能够将用户名与响应关联起来,它需要知道响应中的数组大小将与作为参数传递的数组大小相同,并且响应中的顺序将与参数中的顺序匹配。我们称这些为“复数标识根字段(plural identifying root fields)”,它们的要求如下所述。

字段

符合本规范的服务端可以暴露接受输入参数列表并返回响应列表的根字段。为了让符合规范的客户端使用这些字段,这些字段必须是“复数标识根字段”,并遵循以下要求。

注:符合规范的服务端可以暴露非“复数标识根字段”的根字段;只是符合规范的客户端将无法在其查询中将这些字段用作根字段。

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

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

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

因此,建议服务端不要在响应类型中使用非空封装,因为如果无法获取输入中给定条目的对象,它仍必须在输出中为该输入条目提供一个值;null 是实现此目的的有效值。

下一课

缓存

探索缓存技术和 ID 策略,使客户端性能和对象重用更加高效。

前往下一课 教程