「バージョン管理したい」という要望の正体

業務システムやSaaSを作っていると、PdMやデザイナー、運用担当者からこんな要望が必ずやってきます。

  • 「過去のコンテンツも残しておきたい」
  • 「いつでも前のバージョンに戻せるようにしたい」
  • 「変更履歴を見られるようにしたい」
  • 「v1, v2 みたいに番号を振って管理したい」

これを聞いた瞬間、エンジニアは反射的に content_versions テーブルを設計し始めます。 「contents 本体は最新だけ持って、過去のスナップショットを別テーブルに溜める」「current_version_id でポインタを管理する」—— シンプルそうに見えて、この選択は今後数年にわたってシステム全体を複雑化させる選択です。

本記事では、「バージョン管理」と素朴に呼ばれる機能が実装されるとシステムに何が起きるのかを言語化し、 多くのケースで「複製して新規追加」で十分であることを示します。 その上で、本当にバージョン管理が必要なケースを判別する5つの質問を提示します。

バージョン管理が呼び込む複雑性 — システム編

content_versions テーブルを1つ足すだけ」と思いがちですが、実際にはスキーマ・クエリ・周辺機能まで広範に波及します。

スキーマ設計の分岐

まずスナップショット型か差分型かの選択を迫られます。スナップショット型は毎回コンテンツ全体を複製してDB容量を圧迫し、 差分型はビジネスロジックでマージ処理が必要になり実装が重い。さらに「最新版を指すポインタをどこに置くか」も決めなければなりません。

  • contents.current_version_id: 本体テーブルに最新版IDを持つ(更新時の整合性管理が必要)
  • content_versions.is_current: 各版に「現行フラグ」を立てる(複数行が同時にtrueにならないよう制約が必要)
  • content_versions.published_at: 最も「published_atが新しい」ものを現行とする(クエリが常にサブクエリ付きになる)

どの方式を選んでも、それぞれ異なる種類の整合性問題と運用負荷が発生します。

関連エンティティの宛先問題

さらに深刻なのが関連エンティティが「どの版」を指すかの問題です。 コンテンツに紐づくコメント・いいね・閲覧履歴・通知・お気に入りといったエンティティは、 「コンテンツ本体」を指すべきか、「特定の版」を指すべきか。

コンテンツ本体を指すと、過去版を表示しているのにコメントは現行版のものが表示されてしまいます。 特定の版を指すと、v2 にコンテンツが更新された瞬間にコメントが消えたように見えます。 どちらも業務的に違和感のある挙動になりがちで、結局「コメントは本体に紐づけ、本文表示は本体の最新版から」といった非対称な参照ルールが必要になります。

graph TB
    subgraph version_model["バージョン管理モデル(複雑)"]
      direction TB
      C1["contents"]
      CV["content_versions"]
      P["current_version_id"]
      CMT1["comments"]
      C1 -->|"1:N"| CV
      C1 -.->|"持つ"| P
      P -.->|"指す"| CV
      CMT1 -.->|"どの版?"| CV
    end
    subgraph copy_model["複製モデル(シンプル)"]
      direction TB
      C2["contents (旧)"]
      C3["contents (新)"]
      R["redirects 旧→新"]
      CMT2["comments"]
      CMT2 --> C2
      C3 -.->|"任意"| R
      C2 -.->|"is_archived=true"| C2
    end
バージョン管理モデルは関連エンティティの宛先設計を強いるが、複製モデルは普通の外部キーで済む

スキーマ進化の互換性問題

時間が経つとコンテンツのフィールドは増減します。「タグ機能を追加した」「サムネイル必須に変更した」「カテゴリを別テーブルに切り出した」—— この変更を過去の全バージョンに適用するかという問題が常につきまといます。

適用しなければ、過去版の表示時に必要なフィールドが欠落する。 適用すれば、「v1 のときには存在しなかったタグ」が後から付与されることになり、「過去の状態を保存する」という当初の目的が壊れます。 さらにアプリ側のレンダリングコードに「このフィールドは null かもしれない」分岐が増え続けることになります。

バージョン管理が呼び込む複雑性 — UI・運用編

システム設計だけで終わりません。UIと運用にも継続的なコストが発生します。

編集UIの認知負荷

バージョンを持つコンテンツの編集UIには、必ず以下の要素が必要になります。

  • 「いま編集しているのはどの版か」のインジケータ
  • 過去版への切り替えメニュー
  • 「過去版を編集中ですが保存すると新しい版になります」のような警告
  • 版間の差分表示(diff UI)
  • ロールバック操作(と、ロールバック後にできる新しい版の扱い)

これら全てが、本来「コンテンツを書く・読む」だけの体験に重なってきます。 ユーザーは「コンテンツを編集している」のではなく「バージョン管理ツールを操作している」気分になっていきます。

ストレージ肥大化とガベージコレクション

スナップショット型ならコンテンツ容量はバージョン数に比例して増えます。 画像・添付ファイルを含むコンテンツなら、ストレージコストもバックアップ時間も指数的に増えていきます。

そして必ず発生するのが「どのバージョンが消せるか」という判断不能問題です。 「過去の版を消すと業務影響があるかもしれない」と誰も削除に踏み切れず、5年前の不要な版が残り続ける。 ガベージコレクション機能を作るには「どこからも参照されていない版」を判定する仕組みが必要で、これ自体が別のサブシステムになります。

代替案 — 「複製して新規追加」で済むケース

ここまで読むと「じゃあ過去を残したいときはどうするんだ」と思うはずです。 多くのケースで答えは単純で、新しいコンテンツを別レコードとして追加し、古いものはそのまま残すという方法です。 バージョン管理ではなく、独立した別エンティティとして扱う。

たとえばブログ記事をリライトしたいなら、旧記事はそのままアーカイブし、新記事を別のIDで作成する。 必要なら旧URLから新URLへリダイレクトを設定する。 料金プランを改定するなら、旧プランは archived=true にして残し、新プランを別レコードとして追加する。 既存顧客は旧プランに紐づいたまま、新規顧客から新プランに紐づく。

観点 バージョン管理モデル 複製モデル
スキーマ 本体 + バージョンテーブル + 現行ポインタ 普通のテーブル1つ(status等の状態列のみ)
クエリ 常に「最新版を取る」JOIN/サブクエリが必要 普通のSELECT
関連エンティティ 「どの版を指すか」の設計が必要 普通の外部キー
UI 版切替・差分表示・ロールバックが必須 一覧と通常CRUDで足りる
ストレージ バージョン数に比例して増加 更新で上書き、または新レコード追加分のみ
ガベージ判定 「消せる版」の判別が困難 「アーカイブ済み」を単純に削除可
過去の参照性 版IDで遡れる 旧レコードのIDで遡れる(同等)

本当にバージョン管理が必要なケース

もちろん、バージョン管理が真に必要なケースもあります。判別のためには、以下の5つの質問のうち1つでも「はい」になるかを確認してください。

  1. 過去版を「編集可能な状態で」復元する必要があるか? (単に閲覧できれば足りるなら、read-onlyな履歴ログで十分)
  2. 複数の版が同時に「公開状態」で存在する必要があるか? (顧客ごとに違う版が公開されているなど。1つだけが現行で足りるなら不要)
  3. 版間の差分が業務情報として頻繁に閲覧されるか? (「誰がいつ何を変えたか」が業務上の意思決定に使われる場合のみ)
  4. ロールバックが「ファイルを取り戻す」ではなく「DBレコードを切り戻す」必要があるか? (バックアップからの復元では不可能な、UI操作1回でのロールバックが業務要件か)
  5. 法令や監査要件で「全版の不可変保存」が求められるか? (契約書、医療記録、金融取引など。この場合は「バージョン管理」というより「改ざん不可な履歴ログ」が必要)

これら5つのいずれにも「はい」がないなら、ほぼ確実にバージョン管理は過剰設計です。 代わりに、状態列(draft/published/archived)と「複製して新規追加」、そして必要なら read-only の監査ログを併用するのが現実解になります。

監査ログ ≠ バージョン管理

もう一つ重要な区別があります。「過去を残したい」という要望が実は「監査ログが欲しい」というケースがかなり多いということです。 この2つは目的も実装もまったく違います。

  • バージョン管理: 過去版を編集可能な状態で復元・操作する機能
  • 監査ログ: 過去の変更履歴を読み取り専用で確認する機能

「いつ誰が何を変えたかを後で確認したい」という要望なら、それは監査ログで十分です。 audit_logs テーブルに actor, action, target_id, before, after, occurred_at を記録するだけで、 バージョン管理の複雑性を一切呼び込まずに「過去を残す」という要件を満たせます。 要望をヒアリングする際は「過去を見たいだけですか、それとも過去に戻したいですか」と必ず確認してください。

実装に踏み切る前のチェックリスト

最後に、「バージョン管理機能を作る」という意思決定を下す前に、自分とチームに問うべきチェックリストを置いておきます。

「バージョン管理したい」という言葉は短く、要求として明快に聞こえます。 しかしその裏には、システムと運用の両方を数年にわたって複雑化させる選択が隠れています。 反射的に content_versions テーブルを作り始める前に、本当にそれが必要かを問い、 「複製して新規追加」「監査ログ」「状態遷移」という安価な代替で要件が満たせないかを必ず先に検討する—— これがジュニア〜ミドルエンジニアが現場で身につけるべき設計の作法です。

理解度チェック

理解度チェック

問題 0 / 50%
Q1

業務担当者から「コンテンツのバージョン管理機能が欲しい」と言われたとき、まず最初にすべきことはどれか。

キーボード: 1〜4 で選択、Enter で回答