知海

最佳实践:Node 接口

graphql-github-io-zh-Hans指南与教程

最佳实践:Node 接口

全局对象识别

一致的对象访问实现了简单的缓存和对象查找。

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

为此,客户端需要通过一种标准机制——即使用 ID 请求对象——来进行查询。与此同时,在响应中,schema 也需要提供一种标准方式来呈现这些 ID。

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

graphql 复制代码
{
  node(id: "4") {
    id
    ... on User {
      name
    }
  }
}
  • GraphQL schema 的格式允许通过根查询对象上的 node 字段来获取任何对象。返回的对象符合 Node 接口
  • 可以安全地从响应中提取出 id 字段,并将其存储起来,以便通过缓存和重新获取进行重用。
  • 客户端可以使用接口片段来提取特定于类型且符合节点接口的其他信息。在上述示例中,这个类型为 User

Node 接口定义如下:

graphql 复制代码
# 具有全局唯一 ID 的对象
interface Node {
  # 对象的 ID
  id: ID!
}

User 类型通过以下方式符合该接口:

graphql 复制代码
type User implements Node {
  id: ID!
  # 全名
  name: String!
}

规范

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

保留类型

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

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

Node 接口

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

这个 id 应当是对象的全局唯一标识符,并且只要给定这个 id,服务端就应该能够重新获取该对象。

内省

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

graphql 复制代码
{
  __type(name: "Node") {
    name
    kind
    fields {
      name
      type {
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

返回结果:

json 复制代码
{
  "__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

内省

正确实现上述需求的服务端将接受如下的内省查询,并返回包含所提供响应的响应:

graphql 复制代码
{
  __schema {
    queryType {
      fields {
        name
        type {
          name
          kind
        }
        args {
          name
          type {
            kind
            ofType {
              name
              kind
            }
          }
        }
      }
    }
  }
}

返回结果:

json 复制代码
{
  "__schema": {
    "queryType": {
      "fields": [
        // 该数组可能还有其他条目
        {
          "name": "node",
          "type": {
            "name": "Node",
            "kind": "INTERFACE"
          },
          "args": [
            {
              "name": "id",
              "type": {
                "kind": "NON_NULL",
                "ofType": {
                  "name": "ID",
                  "kind": "SCALAR"
                }
              }
            }
          ]
        }
      ]
    }
  }
}

字段稳定性

如果一个查询中出现两个对象,并且它们都使用相同的 ID 来实现 Node 接口,那么这两个对象必须相等。

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

  • 如果在两个对象上都查询了某个字段,则在第一个对象上查询该字段的结果必须等于在第二个对象上查询该字段的结果。
    • 如果该字段返回一个标量,则相等性定义为该标量的相等性。
    • 如果该字段返回一个枚举,则相等性定义为两个字段都返回相同的枚举值。
    • 如果该字段返回一个对象,则按照上述方法递归地定义相等性。

例如:

graphql 复制代码
{
  fourNode: node(id: "4") {
    id
    ... on User {
      name
      userWithIdOneGreater {
        id
        name
      }
    }
  }
  fiveNode: node(id: "5") {
    id
    ... on User {
      name
      userWithIdOneLess {
        id
        name
      }
    }
  }
}

可能会返回:

json 复制代码
{
  "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 的根字段,它使用用户的用户名为参数并返回对应的用户:

graphql 复制代码
{
  username(username: "zuck") {
    id
  }
}

可能会返回:

json 复制代码
{
  "username": {
    "id": "4"
  }
}

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

graphql 复制代码
{
  usernames(usernames: ["zuck", "moskov"]) {
    id
  }
}

可能会返回:

json 复制代码
{
  "usernames": [
    {
      "id": "4"
    },
    {
      "id": "6"
    }
  ]
}

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

字段

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

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

复数识别根字段必须有且仅有一个参数。该参数的类型必须是非空的非空值列表。在我们的 usernames 示例中,该字段将使用名为 usernames 的单一参数,其类型(使用我们的类型系统速记)将为 [String!]!

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

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

因此,建议服务端不要为响应类型添加非空包装器,因为如果无法为输入中的给定条目获取对象,它仍然必须在输出中为该输入条目提供一个值;对这种情况来说,null 是一个有用的值。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。