GraphQLロゴGraphQL

グローバルオブジェクト識別

一貫性のあるオブジェクトアクセスにより、シンプルなキャッシングとオブジェクトルックアップが可能になります

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 と呼ばれるインターフェースを提供する必要があります。そのインターフェースには、null以外の ID を返す id と呼ばれるフィールドが1つだけ含まれている必要があります。

この 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 という名前のnull以外のIDを1つだけ引数として取る必要があります。

クエリが Node を実装するオブジェクトを返す場合、サーバーによって Nodeid フィールドで返された値が node ルートフィールドに id パラメーターとして渡されると、このルートフィールドは同一のオブジェクトを再取得する必要があります。

サーバーはこのデータの取得に最善を尽くす必要がありますが、常に可能とは限りません。たとえば、サーバーは有効な 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"
}
}
}
]
}
]
}
}
}

フィールドの安定性#

2つのオブジェクトがクエリに表示され、どちらも同じIDで `Node` を実装している場合、2つのオブジェクトは等しくなければなりません。

この定義のために、オブジェクトの等価性は次のように定義されます。

  • 両方のオブジェクトでフィールドが照会される場合、最初のオブジェクトでそのフィールドを照会した結果は、2番目のオブジェクトでそのフィールドを照会した結果と等しくなければなりません。
    • フィールドがスカラーを返す場合、等価性は、そのスカラーに適しているとおりに定義されます。
    • フィールドが列挙型を返す場合、等価性は両方のフィールドが同じ列挙値を返すことで定義されます。
    • フィールドがオブジェクトを返す場合、等価性は上記のように再帰的に定義されます。

例えば

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

クライアントがユーザー名をレスポンスにリンクできるようにするには、レスポンス内の配列が引数として渡された配列と同じサイズであり、レスポンス内の順序が引数内の順序と一致することを知る必要があります。これらを*複数識別ルートフィールド*と呼び、その要件を以下に示します。

フィールド#

この仕様に準拠したサーバーは、入力引数のリストを受け入れ、レスポンスのリストを返すルートフィールドを公開する場合があります。仕様に準拠したクライアントがこれらのフィールドを使用するには、これらのフィールドは*複数識別ルートフィールド*であり、次の要件に従う必要があります。

注:仕様に準拠したサーバーは、*複数識別ルートフィールド*ではないルートフィールドを公開する場合があります。仕様に準拠したクライアントは、それらのフィールドをクエリのルートフィールドとして使用できません。

*複数識別ルートフィールド*には、単一の引数が必要です。その引数のタイプは、null以外のnull以外のリストである必要があります。 `usernames` の例では、フィールドは `usernames` という名前の単一の引数を取り、そのタイプ(タイプシステムの省略形を使用)は `[String!]!` になります。

*複数識別ルートフィールド*の戻り値の型は、リスト、またはリストを囲むnull以外のラッパーである必要があります。リストは、`Node` インターフェース、`Node` インターフェースを実装するオブジェクト、またはそれらのタイプを囲むnull以外のラッパーをラップする必要があります。

*複数識別ルートフィールド*が使用されるたびに、レスポンス内のリストの長さは引数内のリストの長さと等しくなければなりません。レスポンス内の各項目は、入力内の対応する項目に対応する必要があります。より正式には、ルートフィールドに入力リスト `Lin` を渡すと出力値 `Lout` が生成された場合、任意の順列 `P` について、ルートフィールド `P(Lin)` を渡すと出力値 `P(Lout)` が生成される必要があります。

このため、サーバーはレスポンスタイプがnull以外のラッパーをラップしないようにすることをお勧めします。これは、入力内の特定のエントリのオブジェクトを取得できない場合でも、その入力エントリの出力に値を提供する必要があるためです。 `null` は、そうするための便利な値です。

続きを読みます →キャッシング