一致的对象访问使简单的缓存和对象查找成为可能
为了为 GraphQL 客户端提供优雅地处理缓存和数据重新获取的选项,GraphQL 服务器需要以标准化的方式公开对象标识符。
为了实现这一点,客户端需要通过标准机制查询以通过 ID 请求对象。然后,在响应中,模式需要提供一种标准方法来提供这些 ID。
由于除了其 ID 之外,对该对象知之甚少,因此我们将这些对象称为“节点”。以下是一个节点的查询示例
{ node(id: "4") { id ... on User { name } }}
node
字段获取任何对象。这将返回符合“节点”接口的对象。id
字段可以安全地从响应中提取出来,并可以存储以通过缓存和重新获取进行重复使用。节点接口如下所示
# An object with a Globally Unique IDinterface Node { # The ID of the object. id: ID!}
用户通过以下方式符合
type User implements Node { id: ID! # Full name name: String!}
以下所有内容都以更正式的要求描述了围绕对象标识的规范,以确保跨服务器实现的一致性。这些规范基于服务器如何与 Relay API 客户端兼容,但对任何客户端都很有用。
与本规范兼容的 GraphQL 服务器必须保留某些类型和类型名称以支持一致的对象标识模型。特别是,本规范为以下类型创建了指南
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
接口。此根字段必须接受一个参数,一个名为id
的非空ID。
如果查询返回一个实现了Node
的对象,那么当服务器在Node
的id
字段中返回的值作为id
参数传递给node
根字段时,此根字段应该重新获取相同对象。
服务器必须尽力获取此数据,但并非总是可行;例如,服务器可能会返回一个具有有效id
的User
,但当请求使用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.id
和fiveNode.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
是执行此操作的有用值。