1. はじめに
1.1 なぜスキーマの進化とバージョニングが問題になるのか
GraphQLを導入すると、APIは「型の集合」として扱われます。これにより、従来のRESTエンドポイントのように /v1/...
, /v2/...
とURLを増やす手法ではなく、「単一のスキーマを継続的に拡張していく」 というやり方が基本となります。
とはいえ、アプリケーションが成長すると、スキーマの内容を大幅に変更したい、あるいは既存の型やフィールドを削除したいという要望が発生します。これを無計画に行うと「破壊的変更」によって既存クライアントが機能しなくなり、サービス全体に混乱をもたらすおそれがあります。
1.2 RESTとの対比
RESTではしばしば、エンドポイントを世代(バージョン)ごとに増やすやり方がとられます。たとえば /api/v1/users
→ /api/v2/users
のようにURLのパスを変え、古いパスを一定期間サポートした後に廃止するといった運用が一般的です。一方、GraphQLでは「単一のエンドポイントで、拡張・変更を継続する」という思想が重要視されます。これは、
- クライアントが取りに行くデータを柔軟に選べる
- 不要なエンドポイントを乱立させない
というメリットからくる考え方です。
2. GraphQLの基本理念: 破壊的変更をできるだけ避ける
2.1 新しいフィールドや型の追加は非破壊的
GraphQLでは原則、新しい型やフィールドを追加しても、既存クエリに影響を与えないようになっています。なぜなら、クライアントは必要なフィールドのみを明示的に取得する からです。
- 既存クライアントが知らないフィールドが増えても、そのクライアントは新フィールドを要求しない限り、返ってくる結果は今までと同じになります。
- そのため、大半の追加は「非破壊的変更 (non-breaking changes)」とみなされ、後方互換性が保たれます。
2.2 フィールドや型の削除は破壊的変更
一方、既存のフィールドを削除したり、型の構造を大きく変更したりする行為は、破壊的変更 (breaking changes) に分類されます。なぜなら、古いクライアントがまだそのフィールドを利用していた場合、クエリが失敗するなどの問題が発生するためです。
破壊的変更はクライアント側の修正やデプロイを同時に行わなければならないケースが多く、慎重な運用が必要になります。
3. 破壊的変更を最小化する運用: @deprecated での段階的移行
GraphQL公式ドキュメントや多くの事例で推奨される方法として、古いフィールドをいきなり削除せず、@deprecated
ディレクティブを付けて一定期間残す という運用があります。
3.1 @deprecatedの使い方
たとえば以下のように宣言します:
type User {
id: ID!
name: String
"""
@deprecated(reason: "Use `fullName` instead")
"""
legacyName: String
fullName: String
}
-
@deprecated(reason: "...")
によって、「このフィールドは将来削除予定である」という情報をスキーマにも明示できる - GraphiQLやGraphQL Playgroundなどでは、この理由を表示してくれるため、クライアント開発者は「どのフィールドが非推奨か」「代替フィールドは何か」を素早く把握できる
3.2 非推奨化から削除までのタイムライン
- フィールドを非推奨としてマーク
- クライアントに告知(代替フィールドの案内や、移行のデッドラインなど)
- 移行期間を設定(数週間~数カ月など、プロジェクト規模による)
- クライアントが移行を完了(古いフィールドへの依存がなくなったことを確認)
- 実際に削除
この流れを踏むことで、クライアントに猶予を与えつつスキーマのクリーンアップを進めることが可能です。
4. 大きな変更が必要な場合の対応策
4.1 スキーマそのもののバージョンアップは最終手段
どうしても大規模な破壊的変更が必要になったとき、GraphQLサービス全体のバージョンを上げる という手段は存在します。しかし、これは最終手段とみなされます。なぜなら、結局は/graphql
の一つのエンドポイントを持つだけがGraphQLのメリットであり、複数のバージョンを同時運用するとREST同様にエンドポイントが乱立してしまう恐れがあるためです。
- 例:
/graphql?version=2
のようにバージョンIDをクエリパラメータやHTTPヘッダで分岐する - あるいは
/v2/graphql
エンドポイントを別途用意する
これらの方法を採ると、クライアントが「どのGraphQLエンドポイントを使うか」を選ばなければならなくなり、運用の複雑度が上昇します。
4.2 「非破壊的変更」を原則とした拡張で済むケース
実際には、大規模改変が必要と思われたケースでも、以下の工夫によって非破壊的変更にとどめられることが多いです。
-
別の新しい型を追加する
- 古い型は@deprecatedフィールドを含めつつ維持し、新たな型で大幅に構造を変える
- 新型への移行を促す期間を設け、最終的に古い型を削除する
-
入力型(input)にも@deprecated的な扱い
- ミューテーションに使う入力型を新旧で分け、古い方は徐々に廃止へ
-
クライアントが使わないフィールドを把握する
- スキーマの使用状況を観測(たとえば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
を新設したい。 -
ステップ:
-
User
型にfullName
フィールドを追加(既存クライアントは無視する) -
name
に@deprecated(reason: "Use
fullNameinstead.")
を付与 - チーム内でアナウンス、「今後はfullNameを使ってください」と伝える
- 1〜3か月程度の移行期間を取り、クライアント側で
name
→fullName
に切り替え完了 - 移行率が100%になったら
name
を実際に削除
-
6.2 大規模スキーマリファクタ
- 現状: ゆるやかに拡張してきたスキーマが肥大化し、矛盾や重複が目立つ。
- 要望: 意味の重複する型・フィールドを整理して一新したい。
-
アプローチ:
- 大きく変わる箇所を「新型」「新ミューテーション」として追加
- 既存の重複する型には
@deprecated
を付け、クライアントには新型への移行を促す - メトリクスやログを見ながら、古い型・フィールドの利用者がいなくなった段階で順次削除
- 結果的に、単一の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を進化させることが可能になります。