GraphQLにおけるスキーマの進化とバージョニング

## 1. はじめに

### 1.1 なぜスキーマの進化とバージョニングが問題になるのか

GraphQLを導入すると、APIは「型の集合」として扱われます。これにより、従来のRESTエンドポイントのように `/v1/...`, `/v2/...` とURLを増やす手法ではなく、**「単一のスキーマを継続的に拡張していく」** というやり方が基本となります。

とはいえ、アプリケーションが成長すると、スキーマの内容を大幅に変更したい、あるいは既存の型やフィールドを削除したいという要望が発生します。これを無計画に行うと「破壊的変更」によって既存クライアントが機能しなくなり、サービス全体に混乱をもたらすおそれがあります。

### 1.2 RESTとの対比

RESTではしばしば、エンドポイントを世代（バージョン）ごとに増やすやり方がとられます。たとえば `/api/v1/users` → `/api/v2/users` のようにURLのパスを変え、古いパスを一定期間サポートした後に廃止するといった運用が一般的です。一方、GraphQLでは「**単一のエンドポイントで、拡張・変更を継続する**」という思想が重要視されます。これは、

1. **クライアントが取りに行くデータを柔軟に選べる**
2. **不要なエンドポイントを乱立させない**

というメリットからくる考え方です。

---

## 2. GraphQLの基本理念: 破壊的変更をできるだけ避ける

### 2.1 新しいフィールドや型の追加は非破壊的

GraphQLでは原則、新しい型やフィールドを追加しても、既存クエリに影響を与えないようになっています。なぜなら、**クライアントは必要なフィールドのみを明示的に取得する** からです。

- 既存クライアントが知らないフィールドが増えても、そのクライアントは新フィールドを要求しない限り、返ってくる結果は今までと同じになります。
- そのため、大半の追加は「**非破壊的変更** (non-breaking changes)」とみなされ、後方互換性が保たれます。

### 2.2 フィールドや型の削除は破壊的変更

一方、既存のフィールドを削除したり、型の構造を大きく変更したりする行為は、**破壊的変更 (breaking changes)** に分類されます。なぜなら、古いクライアントがまだそのフィールドを利用していた場合、クエリが失敗するなどの問題が発生するためです。

破壊的変更はクライアント側の修正やデプロイを同時に行わなければならないケースが多く、慎重な運用が必要になります。

---

## 3. 破壊的変更を最小化する運用: @deprecated での段階的移行

GraphQL公式ドキュメントや多くの事例で推奨される方法として、**古いフィールドをいきなり削除せず、`@deprecated` ディレクティブを付けて一定期間残す** という運用があります。

### 3.1 @deprecatedの使い方

たとえば以下のように宣言します:

```graphql

type User {
  id: ID!
  name: String
  """
  @deprecated(reason: "Use `fullName` instead")
  """
  legacyName: String
  fullName: String
}

```

- `@deprecated(reason: "...")` によって、「このフィールドは将来削除予定である」という情報をスキーマにも明示できる
- GraphiQLやGraphQL Playgroundなどでは、この理由を表示してくれるため、クライアント開発者は「どのフィールドが非推奨か」「代替フィールドは何か」を素早く把握できる

### 3.2 非推奨化から削除までのタイムライン

1. **フィールドを非推奨としてマーク**
2. **クライアントに告知**（代替フィールドの案内や、移行のデッドラインなど）
3. **移行期間を設定**（数週間～数カ月など、プロジェクト規模による）
4. **クライアントが移行を完了**（古いフィールドへの依存がなくなったことを確認）
5. **実際に削除**

この流れを踏むことで、クライアントに猶予を与えつつスキーマのクリーンアップを進めることが可能です。

---

## 4. 大きな変更が必要な場合の対応策

### 4.1 スキーマそのもののバージョンアップは最終手段

どうしても大規模な破壊的変更が必要になったとき、**GraphQLサービス全体のバージョンを上げる** という手段は存在します。しかし、これは**最終手段**とみなされます。なぜなら、結局は`/graphql` の一つのエンドポイントを持つだけがGraphQLのメリットであり、複数のバージョンを同時運用するとREST同様にエンドポイントが乱立してしまう恐れがあるためです。

- 例: `/graphql?version=2` のようにバージョンIDをクエリパラメータやHTTPヘッダで分岐する
- あるいは `/v2/graphql` エンドポイントを別途用意する

これらの方法を採ると、クライアントが「どのGraphQLエンドポイントを使うか」を選ばなければならなくなり、運用の複雑度が上昇します。

### 4.2 「非破壊的変更」を原則とした拡張で済むケース

実際には、大規模改変が必要と思われたケースでも、以下の工夫によって**非破壊的変更**にとどめられることが多いです。

1. **別の新しい型を追加する**
    - 古い型は@deprecatedフィールドを含めつつ維持し、新たな型で大幅に構造を変える
    - 新型への移行を促す期間を設け、最終的に古い型を削除する
2. **入力型（input）にも@deprecated的な扱い**
    - ミューテーションに使う入力型を新旧で分け、古い方は徐々に廃止へ
3. **クライアントが使わないフィールドを把握する**
    - スキーマの使用状況を観測（たとえばApollo Studio/Apollo GraphOSのUsage Tracking機能やGraphQL Inspectorなど）して、実際に使用されていないフィールドは破壊的変更にならない
    - 使っているクライアント数が極小のフィールドなら、削除のインパクトも小さい

これらを駆使すれば、エンドポイント自体を複数用意せずとも段階的な移行が可能です。

---

## 5. スキーマバージョニングの実装戦略

「単一スキーマを進化させる」ことと「時には破壊的変更も受け入れる」ことの両立は、以下のような運用上のテクニックで実現されます。

### 5.1 スキーマ差分の検出

大規模プロジェクトでは、**スキーマの変更が後方互換性を壊していないか**を自動チェックする仕組みがあると便利です。

- **GraphQL Inspector** (OSSツール) は、新旧スキーマを比較し、破壊的変更がないか、`@deprecated` が正しく宣言されているかなどをレポートしてくれます。
- CIパイプラインに統合し、プルリクエスト単位でスキーマ変更をレビューする運用が推奨されます。

### 5.2 deprecation workflow の明文化

スキーマを変更するたびに都度やり方を悩むのではなく、**組織として「deprecation手順」を文書化**しておくとスムーズです。

- 「古いフィールドを@deprecatedにするときは最低1か月前にSlackで通知」
- 「1か月後にクライアント数が0になれば削除OK」
- 「それ以上残っているなら期限を延長するか、関係者に直接確認」のような手続きを定めておけば、大規模開発でも混乱を避けられます。

### 5.3 スキーマリリースとクライアントリリースの連携

GraphQLの破壊的変更を行う場合、**サーバーとクライアント双方のリリース**が連動します。

- 先にサーバーが@deprecatedを付与 → 新しいクライアント実装をデプロイ → 一定期間後に古いクライアントがなくなってから、サーバーの該当フィールドを削除
- レガシークライアントが消えていない状態で削除すると障害が起きるので、計画的なスケジュール管理が必須となります

クラウドサービスやネイティブアプリでは特に、クライアント側がすべて更新されるまで古いフィールドを残しておく必要がある点を忘れないようにしましょう。

---

## 6. 具体的な事例と運用例

### 6.1 ユーザー名フィールドの変更例

- **現状**: `User` 型に `name` フィールドがあり、既存クライアントが広く利用している。
- **要望**: `name` の代わりに `firstName`, `lastName` を分けたい、または `fullName` を新設したい。
- **ステップ**:
    1. `User` 型に `fullName` フィールドを追加（既存クライアントは無視する）
    2. `name` に `@deprecated(reason: "Use` fullName `instead.")` を付与
    3. チーム内でアナウンス、「今後はfullNameを使ってください」と伝える
    4. 1〜3か月程度の移行期間を取り、クライアント側で `name` → `fullName` に切り替え完了
    5. 移行率が100%になったら `name` を実際に削除

### 6.2 大規模スキーマリファクタ

- **現状**: ゆるやかに拡張してきたスキーマが肥大化し、矛盾や重複が目立つ。
- **要望**: 意味の重複する型・フィールドを整理して一新したい。
- **アプローチ**:
    1. 大きく変わる箇所を「新型」「新ミューテーション」として追加
    2. 既存の重複する型には`@deprecated`を付け、クライアントには新型への移行を促す
    3. メトリクスやログを見ながら、古い型・フィールドの利用者がいなくなった段階で順次削除
    4. 結果的に、単一のGraphQLエンドポイントのままで新構造へ段階的に移行完了

大幅にスキーマを変えたにもかかわらず、クライアントごとに「どのエンドポイントを呼ぶか」を選択する必要がなかった、という点がGraphQLの利点となります。

---

## 7. バージョン管理とドキュメント

### 7.1 スキーマのバージョンは持たない？

前述のとおり、GraphQLの公式な推奨としては「**スキーマは原則1つ**」であり、拡張を続けていくことが前提です。そのため、個別に「v1」「v2」とスキーマに冠を付けるのはアンチパターンとされています。ただし、運用上、何らかの理由でどうしても切り替えが必要になる場合、以下のように柔軟にする例はあります。

- サブドメインを分けて `/graphql` と `/api-experimental/graphql` を用意する
- HTTPヘッダ `X-GraphQL-Version: 2` でスキーマを切り替える仕組みを実装する
- Feature Flagを用いて一部のクライアントだけ新スキーマを見られるようにする

いずれにせよ、これらは例外的な措置で、スキーマの後方互換性を可能な限り保つのが主流の方針です。

### 7.2 スキーマのドキュメント化と変更履歴

GraphQLスキーマは、**イントロスペクション**や**トリプルクォートによるドキュメントコメント**によって、自動的にドキュメントとして公開できます。さらに、変更履歴（チェンジログ）をきちんと管理すると、クライアント開発者がいつ、どのフィールドが非推奨になったのかを把握しやすくなります。

- Gitでコミットログを追えばスキーマ変更点が履歴として残る
- あるいはドキュメント専用のレポジトリ、またはConfluenceなどのドキュメント管理ツールに差分情報をまとめる

特に大規模チームでは「どのプロダクト/サービスがいつまでに移行しなければならないか」を計画的に共有する仕組みが必要です。

---

## 8. まとめ

GraphQLでスキーマをバージョニングする際の原則は**「単一スキーマを継続的に拡張し、破壊的変更は極力避ける」** ということです。

- 新規フィールドや型の追加は後方互換性を壊さずに行える
- 古いフィールドを@deprecatedで告知し、十分な移行期間を設ける
- 大規模な改変が必要な場合も、別の新型や新しいフィールドを追加し段階的に移行する方法が多用される
- どうしても完全に破壊的な大幅変更が必要な場合は、複数バージョンのスキーマ共存を検討するが、GraphQLのメリットを損ないかねないため最終手段と考える

この運用を成立させるには、クライアント開発チームやプロダクトチームへの周知、deprecation期間の確保、古いクエリ使用状況のモニタリングなど、**チーム全体での合意とプロセス**が必要です。しかし、この仕組みがきちんと機能すれば、RESTのように複数バージョンのエンドポイントを乱立させることなく、**継続的にAPIを進化させる**ことが可能になります。