一致的对象访问实现了简单的缓存和对象查找
为了提供给 GraphQL 客户端选项以优雅地处理缓存和数据重新获取,GraphQL 服务端需要以标准化的方式公开对象标识符。
为此客户端需要通过以 ID 请求对象的标准机制进行查询。然后在响应中,schema 将需要提供一种提供这些 ID 的标准方式。
因为除了 ID 之外,对对象知之甚少,所以我们称它们为对象“节点”。这是一个查询节点的示例:
{node(id: "4") {id... on User {name}}
node
字段获取任何对象。返回的对象符合 “Node” 接口。id
字段,并可以通过缓存和重新获取将其存储以供重用。Node 接口如下:
# 具有全局唯一 ID 的对象interface Node {# 对象的 IDid: ID!}
User 通过以下方式符合接口:
type User implements Node {id: ID!# 全名name: String!}
下面的所有内容以更加正式的要求描述了围绕对象标识的规范,以确保在服务端实现之间的一致性。 这些规范是基于服务端如何与 Relay API 客户端兼容来写的,但是对任何客户端都有用。
与此规范兼容的 GraphQL 服务端必须保留某些类型和类型名称,以支持一致的对象标识模型。特别地,此规范为以下类型创建了准则:
Node
的接口。node
字段。服务端必须提供一个名为 Node
的接口。该接口必须有且仅有一个名为 id
的字段,该字段返回非空的 ID
。
这个 id
应当是该对象的全局唯一标识符,并且只要给出这个 id
,服务端就应该能够重新获取该对象。
正确实现上述接口的服务端将接受如下的内省查询,并返回提供的响应:
{__type(name: "Node") {namekindfields {nametype {kindofType {namekind}}}}}
返回
{"__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 {nametype {namekind}args {nametype {kindofType {namekind}}}}}}}
返回
{"__schema": {"queryType": {"fields": [// 该数组可能还有其他条目{"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 {nameuserWithIdOneGreater {idname}}}fiveNode: node(id: "5") {id... on User {nameuserWithIdOneLess {idname}}}}
可能会返回:
{"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
是一个有用的值。