GraphQL スキーマ ボイラープレートなしで SDL を書く、フォーマットし、理解する
GraphQL スキーマは、サーバーとクライアントの間の自己記述型契約です。このガイドでは、スキーマ定義言語(SDL)から始め、タイプ、スケーラー、クエリ、ミューテーション、入力タイプ、エナム、インターフェースについて詳しく解説します。これにより、スキーマを読む・書くことができるようになります。
REST API に慣れていて、初めて GraphQL スキーマに出会った場合、構文は馴染みがあるものの、正確にパースできないかもしれません。これは普通です。スキーマ定義言語(SDL)は簡潔ですが、1行あたり多くの意味を含んでいます。この記事ではそれを分解し、ドキュメントを5分ごとに参照する必要なく、スキーマを読む・書くことができます。
GraphQL スキーマが実際に何であるか
GraphQL スキーマは、API サーバーと呼び出すすべてのクライアントの間の契約です。データの存在、利用可能な操作、および応答の構造を正確に定義します。REST では、エンドポイントをドキュメントや試行錯誤によって発見しますが、GraphQL ではこの契約を明示的に、マシンが読み取れる形で定義します。
すべての GraphQL API は、SDL で書かれたスキーマから始まります。サーバーは実行時にすべてのクエリをこのスキーマに照らして検証します。クライアントがスキーマに存在しないフィールドを要求した場合、リゾルバーが実行される前にリクエストは拒否されます。これは GraphQL の型システムの核心的な価値です:スキーマが API の表面領域の「真実の源」になります。
SDL の基本:型、クエリ、およびミューテーション
すべての SDL ファイルは型定義から構成されます。以下は最も有用なスキーマの最小例です:
type Query {
hello: String
}
Query 読み取り操作のエントリポイントです。すべてのスキーマには1つが必要です。フィールド hello は String — 5つの組み込みスケーラー型のいずれかです。
5つの組み込みスケーラーは以下の通りです:
- String — UTF-8 文字列
- Int — 32ビット符号付き整数
- Float — 64ビット浮動小数点数
- Boolean — true または false
- ID — 一意の識別子で、文字列としてシリアル化される
実際のスキーマでは、カスタムオブジェクト型が定義されます。以下はより一般的なパターンです:
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String
author: User!
publishedAt: DateTime
}
type Query {
user(id: ID!): User
posts: [Post!]!
}
それらの20行に多くのことが起こっています。それらを分解してみましょう。
非空フィールドとリスト
の ! 型の後に記述された場合、フィールドは非空である—それは空を返すことはありません null。記述がない場合、フィールドは空値を許容し、クライアントは欠落した値を処理する必要があります。実行時においてこの違いは重要です:非空フィールドのリゾルバーが例外を投げたり、nullを返した場合、GraphQL はその null をツリーの上に伝播し、親フィールドをすべて null にすることがあります。
リストは角括弧を使用します: [Post] は空値を許容する、空値を許容する投稿のリストです。 [Post!]! は空値を許容しない、空値を許容しない投稿の非空リストです。これは通常のバージョンです—フィールドが常に配列(空であっても)を返し、その配列内の各要素が常に実際のオブジェクトであり、nullではないというものです。
4つの組み合わせを明確に述べると:
[Post]— 空値を許容するリスト、空値を許容する項目(ほとんど役に立たない)[Post!]— 空値を許容するリスト、空値を許容しない項目[Post]!— 空値を許容しないリスト、空値を許容する項目[Post!]!— 空値を許容しないリスト、空値を許容しない項目(最も一般的)
ミューテーション:データの書き込み
クエリは読み取り専用です。ミューテーションは書き込みを処理します。型 Mutation は Queryと同じ構造を持ちますが、慣例上、そのフィールドは副作用を持ちます:
type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
知らせ CreatePostInput! — これは入力型であり、次の概念に直接つながります。
入力型と出力型
これは GraphQL スキーマにおける最も一般的な混乱の一つです。あなたは オブジェクト型のように をミューテーションの引数として再利用できません。SDL には、この理由から2つの種類の型があります: Post オブジェクト型
- ) — レスポンスで使用されます。リゾルバーと複雑なフィールド論理を含めることができます。 (
type入力型 - ) — 引数で使用されます。シンプルなデータであり、リゾルバーはなく、フィールドはスケーラーまたは他の入力型にのみ参照できます。 (
inputこの分離は、引数のバリデーションを明確にし、出力型(複雑なグラフで相互参照する可能性がある)を直接入力として使用すると生じる循環参照問題を防ぎます。
input CreatePostInput {
title: String!
body: String
authorId: ID!
}
カスタムスケーラー
5つの組み込みスケーラーはすべてをカバーしません。日時文字列、URL、JSON ブロブなどはカスタムスケーラーが必要です。それらは次のようになります:
スケーラーの宣言は、SDL に使用する名前を示します。実際のシリアル化、デシリアライズ、およびバリデーションの論理はサーバー実装にあり、スキーマファイルにはありません。ライブラリのような
scalar DateTime
scalar JSON
scalar URL
は、一般的な型に対して準備された実装を提供し、それらをゼロから書く必要はありません。 graphql-scalars カスタムスケーラーを使用する場合、
が技術的には正しいが、意味的に誤っている場合です。フィールドが String としてタイプ化されている場合、意図を伝えるが、フィールドが DateTime としてタイプ化されている場合、クライアントがフォーマットを推測しなければなりません。 String エナム
エナムはフィールドを固定された文字列値のセットに制限します。これは、スケーラーがタイプレベルで許容される値を強制するため、raw string よりも信頼性が高いです:
フィールドが
enum UserRole {
ADMIN
EDITOR
VIEWER
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
としてタイプ化されている場合、予期しない文字列は決して返されません。フィールドが UserRole としてタイプ化されている場合、何でも返す可能性があります。値のセットが知られていて、安定し、クライアントにとって意味のある場合、エナムを使用してください—これはスキーマのインテリジェンスを大幅に強化します。 String インターフェースとユニオン
フィールドが異なるオブジェクト型を返す場合、GraphQL は2つのメカニズムを提供します:インターフェースとユニオン。
インターフェース
共通のフィールドセットを定義します。インターフェースを実装するタイプは、それらのフィールドを含む必要があります: ユニオン
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Post implements Node {
id: ID!
title: String!
}
共通のフィールドを持たないタイプをグループ化します。可能なタイプが構造的に共通点がない場合に便利です: クライアントは、ユニオンまたはインターフェースの各タイプを処理するためにインラインフラグメントを使用します:
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
. ポリモーフィックデータを返す場合、タイプが共通のフィールドを持つ場合はインターフェースを、持たない場合はユニオンを好むべきです。 ... on User { name }ディレクティブ
ディレクティブは、フィールドまたはタイプレベルで動作を変更します。すべての GraphQL 実装に2つが組み込まれています:
— インテリジェンスでフィールドを非推奨にマークする
@deprecated(reason: "Use newField instead")— クエリ内のクライアント側条件分岐@skip(if: Boolean)と@include(if: Boolean)認証ガード、レート制限ヒント、キャッシュ注釈などのためのカスタムディレクティブを定義できます。それらはスキーマに宣言され、サーバーで実装されます:
GraphQL と REST:どちらを使うべきか
directive @auth(requires: UserRole = ADMIN) on FIELD_DEFINITION
type Mutation {
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
}
GraphQL のスキーマファーストアプローチは、特定の状況で効果を発揮します。次の状況で適切です:
複数のクライアント(ウェブ、モバイル、第三者)が同じデータの異なるサブセットを必要とする
- API を進化させたい場合—バージョニングなしにフィールドを追加し、削除するのではなく、非推奨にする
- ドメインが自然にグラフ型で、エンティティ間の複雑な関係を持つ
- クライアントがインテリジェンスを使ってAPIを探索できる、自己説明型のAPIを必要とする
- REST は次の状況でより良い選択肢です:
シンプルなCRUDエンドポイントを構築し、予測可能な平坦なパラメータを必要とする
- HTTPキャッシュが重要—GraphQL POSTリクエストはデフォルトでキャッシュされません
- チームが小さく、APIの表面が安定している—RESTのオーバーヘッドは低い
- N+1問題に直面しており、DataLoaderを設定して解決したい場合
- N+1問題は一時的に停止すべきです。GraphQLリゾルバーが
と各投稿の posts を取得する場合、単純な実装では1つの投稿ごとに1つのデータベースクエリを実行して著者をロードします。50投稿の場合、1つのリクエストに対して51クエリが発生します。RESTでは、各エンドポイントで正確に実行されるSQLを制御できるため、このような問題はありません。GraphQLでは、バッチ処理(DataLoaderまたは同等)で解決します—これは解決可能ですが、正しく設定するためのもう一つの要素です。 authorSDLをフォーマットし、バリデーションする
スキーマファイルは、チームが成長するにつれて、不一致なインデント、間違ったスペース、構造のズレを蓄積します。スキーマ変更をコミットする前に、フォーマッターを使ってスペースを標準化し、フィールドを並べ替え、誤った構文エラーをCIに前に出して検出します。
iotools.cloudはこれを行います—SDLを貼り付けて、きれいで一貫したフォーマットされた出力と、インラインで表示されるバリデーションエラーを返します。複数タイプスキーマ、カスタムスケーラー、ディレクティブ、そして手動フォーマットで発生するエッジケースを処理します。他のチームと共有する前にスキーマをコミットする前の最終チェックとして非常に有用です。
の GraphQLスキーマフォーマッタ GraphQL スキーマ:ボイラープレートなしでSDLを書く、フォーマットし、理解する 2
