Dev Study
← 開発の実務
レッスン 11 / 12·約 8 分·第16期 開発の実務

git revert が戻すのはコードで、データではない

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回は、ブランチの緑がどの基点に載せた結果なのかを見ました。今回は時間を逆に進めます。前の版に戻したとき、その版はいまのデータと比べて壊れていないのか。

まず結論

前の版に戻したコードが「壊れていない」と言えたのは、当時の形のデータと組み合わせたときの話です。この回が比べるのは、戻した旧コードと、そのあいだに形の変わった新しいデータです。

git revert が戻すのはリポジトリの中のコードだけで、データベースの表の形も、戻すまでに書かれた行も、別の手順を書かない限りそのまま残ります。戻せる期間は、変更の出し方で作れます。

? 読む前に予測

表の列 fullname を display_name に改名し、コードも新しい列名に合わせて出しました。不具合が見つかったので git revert HEAD でコードを旧版に戻し、旧版で1行追加します。何が起きるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

git revert が戻す範囲

このコースでは「壊れていない」を、何と比べたかで言い直してきました。前の版に戻すときも同じです。戻す先の版は、かつてテストが緑で、本番でも動いていました。ただしその比較の相手は、当時の形のデータです。

git revert の説明はこうです。

Given one or more existing commits, revert the changes that the related patches introduce, and record some new commits that record them.

打ち消すのは「そのコミットが持ち込んだ変更」で、対象はリポジトリの中身です。データベースのファイルは普通リポジトリに入っていません。入っていないものは、revert の範囲の外です。

前の版に戻したとき、何が戻り、何が残るか
旧版のコード改名前の列を読み書きする
旧い形の表fullname 列
列を改名し、新版を出す
新版のコードdisplay_name を読み書きする
新しい形の表display_name 列、行も増える
git revert HEAD
旧版のコードリポジトリの中なので戻る
新しい形の表のままリポジトリの外なので残る
  • ・最後の段の組み合わせは、旧版がテストされたことのない組み合わせ
  • ・表を戻すには逆向きの手順を別に書く必要がある。書けば戻せる
具体実物

列を改名してから、コードだけを戻す

sqlite3 3.50.6 と git 2.50.1(macOS)で、表 users に1行ずつ足すだけの app.sh を用意しました。旧版は fullname 列に書きます。DB ファイルはリポジトリの外に置きます。列を改名し、新版を出し、git revert で戻します。

実演 A の生出力(抜粋。revert 出力のハッシュのみ伏せた)コマンド出力
== 移行: ALTER TABLE users RENAME COLUMN fullname TO display_name
-- schema: CREATE TABLE users(id INTEGER PRIMARY KEY, display_name TEXT NOT NULL)
== v2: ./app.sh 'Tanaka Jiro'
1|Sato Hanako
2|Suzuki Ichiro
3|Tanaka Jiro
exit=0
== 不具合が見つかったので git revert HEAD
[main <hash>] Revert "v2 app: use display_name"
Date: Thu Jan 1 09:00:00 2026 +0000
1 file changed, 2 insertions(+), 2 deletions(-)
== 戻したコード(v1)で: ./app.sh 'Ito Saburo'
Error: in prepare, table users has no column named fullname
exit=1
-- schema: CREATE TABLE users(id INTEGER PRIMARY KEY, display_name TEXT NOT NULL)

revert が変えたのは 1 file changed の app.sh だけです。最後の行で表を見直すと、まだ display_name です。旧版は正しく戻り、表も正しく新しい形で、組み合わせだけが一度も比べられていません。

✓ ここまでの確認

実演 A で git revert HEAD を実行した直後の状態として正しいのはどれですか。

抽象原理

戻せる期間を作る — expand, migrate, contract

表を先に戻す手順を書く、という手はあります。Sadalage らは、強い構成管理があれば it's not difficult to revert should the worst happen と書いています。「DB の変更は戻せない」は原典と逆向きの言い過ぎです。 ただ、表を戻しても、新版が書いた行の扱いは残ります。

もう1つの手は、旧版がいまの表と組んでも動く期間を、変更の出し方で作ることです。Danilo Sato はこれを3段に分けます。

Parallel change, also known as expand and contract, is a pattern to implement backward-incompatible changes to an interface in a safe manner, by breaking the change into three distinct phases: expand, migrate, and contract.

expand で新旧両方を受け付けるようにし、migrate で利用者を新しい側へ移し、Once all usages have been migrated になってから contract で古い側を消します。Sato は Most database refactorings follow the parallel change pattern とも書いています。Sadalage らはこの両対応の期間を transition phase と呼びます。

第14期「ではどこに線を引くか」では、呼んでいる相手を全部列挙できるかで引き返せるかが決まると見ました。contract に進めるかも、古い列を使う相手が残っていないと言えるかで決まります。

具体実物

同じ改名を、3段に分けて出す

同じ表で、改名の代わりに display_name 列を足し(expand)、新版は両方の列に書くようにして既存行を埋めます(migrate)。そこで不具合が見つかったとして、また git revert で旧版に戻します。

実演 B の生出力(抜粋)コマンド出力
== 戻したコード(v1)で: ./app.sh 'Ito Saburo'
1|Sato Hanako
2|Suzuki Ichiro
3|Tanaka Jiro
4|Ito Saburo
exit=0
== ただし v1 が書いた行は display_name が空:
id|fullname|display_name
1|Sato Hanako|Sato Hanako
2|Suzuki Ichiro|Suzuki Ichiro
3|Tanaka Jiro|Tanaka Jiro
4|Ito Saburo|

今度は旧版が動きます。古い列が残っているからです。ただし旧版は新しい列を知らないので、4番の行は新しい列が空です。戻せても、戻していた間のデータは直す仕事が残ります。

続きには、予定していなかった結果も出ました。

実演 B の続き(抜粋。revert の revert と埋め直しの手順、途中の成功した実行を省いた)コマンド出力
== v3(新しい列だけ使う)を出す。古い列が NOT NULL のままなので:
Error: stepping, NOT NULL constraint failed: users.fullname (19)
exit=19
== contract: 古い列を消す
-- schema: CREATE TABLE users(id INTEGER PRIMARY KEY, display_name TEXT)
== contract 後に v2 へ戻すと(git revert HEAD): ./app.sh 'Hayashi Rokuro'
Error: in prepare, table users has no column named fullname
exit=1

新しい列だけに書く版を contract より先に出すと、古い列の NOT NULL で落ちました。古い列を残す間は、その制約も互換性の条件です。 Sadalage らが非NULL化を destructive change の例に挙げる理由(既存コードが値を入れなければエラー)の裏返しです。そして contract の後に戻すと、実演 A と同じ失敗に戻ります。戻せる期間は contract で終わります。

✓ ここまでの確認

expand 後・contract 前に旧版へ戻し、旧版が4番の行を書きました。この行について正しいのはどれですか。

⇄ 2つを見比べる
具体例 A

一度に改名してから戻す

改名した瞬間に旧版の列が消えます。コードを戻すと旧版は新しい形の表に当たり、列が無いと言って落ちます。旧版が試されたことのない組み合わせです。

戻したあとの旧版コマンド出力
Error: in prepare, table users has no column named fullname
exit=1
具体例 B

expand の後、contract の前に戻す

古い列が残っているので旧版は動きます。ただし戻していた間の行は新しい列が空になり、再び新版へ進む前に埋め直す手順が要ります。contract の後は A と同じ失敗に戻ります。

戻したあとの旧版コマンド出力
4|Ito Saburo
exit=0

同じ git revert HEAD が、片方では落ち、片方では動きました。何が違ったのでしょうか。

revert の前後で、変わらなかったもののほうを見てください。

この章のまとめ

  • 旧版が緑だったのは当時の形のデータと比べたとき。戻した瞬間、旧コードと新データという未比較の組み合わせになる。
  • git revert はコミットの変更を打ち消す新しいコミットを記録する。表の形と、戻すまでに書かれた行はリポジトリの外。
  • 「DB は戻せない」は言い過ぎ。逆向きの手順を別に書けば戻せる。
  • expand → migrate → contract で、旧版が動く戻せる期間を作れる。期間は contract で終わる。
  • 戻していた間の行は新しい列が空になる。古い列の制約も互換性の条件(実演で NOT NULL に当たった)。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

expand の後、contract の前に、新しい列だけに書く版(v3)を出したところ、挿入が失敗しました。原因として実演で確認されたのはどれですか。

用語集(4語)
前の版に戻す(デプロイのロールバック)
動いているアプリを直前の版に差し替えること。トランザクションのロールバックとは別。戻るのは差し替えた対象だけ。
Parallel Change(expand and contract)
後方互換でない変更を expand(新旧両方を受け付ける)・migrate(利用者を新しい側へ移す)・contract(古い側を消す)の3段に分ける型。Danilo Sato, 2014。
destructive change
既存のコードを壊しうるスキーマ変更。Sadalage らの用語。列の追加は多くの場合これに当たらず、非NULL化は小さな destructive change の例。
transition phase
データベースが古いアクセスと新しいアクセスを同時に受け付ける期間。Sadalage らの用語。