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 欄位傳回的值傳遞為 node 根欄位的 id 參數時,此根欄位應重新擷取相同的物件。

伺服器必須盡力擷取此資料,但並非總是可行;例如,伺服器可能會傳回具有有效識別碼的使用者,但當提出要求以節點根欄位重新擷取該使用者時,使用者的資料庫可能無法使用,或使用者可能已刪除其帳戶。在此情況下,查詢此欄位的結果應為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"
}
}
}
]
}
]
}
}
}

欄位穩定性#

如果兩個物件出現在查詢中,兩個物件都實作具有相同識別碼的節點,則兩個物件必須相等。

就這個定義而言,物件相等定義如下

  • 如果在兩個物件上查詢欄位,則在第一個物件上查詢該欄位的結果必須等於在第二個物件上查詢該欄位的結果。
    • 如果欄位傳回純量,則相等定義為適合該純量的相等。
    • 如果欄位傳回列舉,則相等定義為兩個欄位都傳回相同的列舉值。
    • 如果欄位傳回物件,則相等定義為根據上述遞迴定義。

例如

{
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 是用於這樣做的有用值。

繼續閱讀 →快取