俺は宣言性重視のmigrationツールが嫌いだ
「だってmigrationという文字列からもう手続きに本質を感じるじゃん?」
## このドキュメントはなに?
mysqldefをベタに使っていて特にCIや移行リスク検知の仕組みも手薄なプロジェクトのmigartion体制が気になって、どうしてゆくべきかをリサーチした。プロジェクト内ではIssueとして起票しており、執筆はClaude Codeにやらせた。
文章構成は4回くらいレビューしたので、実質的に俺の文章と言えるだろう(と言って手書きしなくなると瞬く間に文章が下手になるんだろうね)
嫌いだと言いつつ、宣言的ツール→宣言的ツールの移行ではある。プロジェクトの方針を尊重しつつ、筆者が感じるペインを解消しようという意図でそのようにした。
ただ、移行先のAtlasは手続き的ツール派の人間が気になるポイントを上手く解消している。その点で、タイトルにはちゃんと応えているつもりだ。そうか?
## 概要
現在のmysqldefベースのマイグレーション基盤を段階的に改善し、より安全なデプロイを実現する提案です。
mysqldefの「migrate.sql を編集するだけ」というシンプルさはプロジェクト初期から大きなメリットでした。バージョン番号管理や差分ファイルの管理が不要で、スキーマの最終状態が常に1ファイルで見えるのは今でも優れた点です。
一方で、プロジェクトが成長しテーブル間のリレーションやデータ移行が複雑化する中で、mysqldefの宣言的モデルだけではカバーしきれないケースが出てきています。
加えて、Coding Agentを活用した高速開発が進む中で、スキーマ変更を含む機能開発のスピードも上がっています。開発速度が上がっても、安全なデータ移行を確信して進められなければ、そこがボトルネックになります。安全性を早期にチェックして高速に移行を進められる体制を作ることは、この高速開発時代において特に重要な取り組みです。
## mysqldef の強みと、現在見えている課題
mysqldef の強み(引き続き活かしたい点)
migrate.sql 1ファイルで「あるべきスキーマ」が一覧できる
バージョン番号管理が不要で認知負荷が低い
単純な追加操作(カラム追加、テーブル追加、インデックス追加)は非常に楽
--dry-run フラグで実行予定のALTER文を事前確認できる(※現在CIでは未活用)
## 課題1:DDLとDMLの順序制御ができない
マイグレーションは本質的に 手続き です。以下のようなケースで、mysqldefの宣言的モデルでは安全な移行ができません。
### 例:正規化のためにカラムを別テーブルに分離するケース
orders テーブルの status_name (VARCHAR) を status_id (FK → order_statuses) に移行する場合、mysqldef が実行するのは
code:sql
ALTER TABLE orders DROP COLUMN status_name; -- ここでデータ消失
ALTER TABLE orders ADD COLUMN status_id bigint(20);
本来必要な手順
code:_
1. status_id カラムを追加(NULLable)
2. order_statuses テーブルを作成し、旧 status_name の値から投入
3. 旧 status_name の値を使って status_id を設定
4. 全行の status_id が正しく設定されたことを確認
5. status_id に NOT NULL / FK 制約を追加
6. 旧 status_name カラムを削除
このとき、DDL → DML → DDL → DML → DDL という手続きが必要ですが、mysqldefでは1ファイルの最終状態しか書けないため、この中間ステップを表現できません。
現状は「PRを2つ以上に分けて順番にデプロイ」で対処していますが、手順ミスのリスクが残ります。
## 課題2:CIでの事前検証が不十分
mysqldef には --dry-run があり、ALTER文を事前確認できます。しかし現在のCIではこの機能を使っておらず、空のテストDBに対して mysqldef を実行するだけです。
つまり「空DBに対してスキーマが適用できること」は検証できていますが、「データが入った本番DBに対して何が起きるか」は検証できていません。mysqldefの --dry-run 自体は有用な機能なので、最低限これをCIに組み込むだけでも改善になります。
## 課題3:破壊的変更の検出が人力依存
PRレビューで migrate.sql の diff を見て「これはDROPが発生するな」と気づけるかどうかは、レビュアーの経験に依存します。カラム名のtypo修正のつもりが実際にはDROP+ADDとして実行されるケースなど、diffからは読み取りにくい変更もあります。
## 提案:Atlas による段階的な改善
Atlas は mysqldef と同じ宣言的スキーマ管理をベースにしつつ、mysqldefの弱点を補う機能を追加で提供するツールです。 table:_
機能 mysqldef Atlas
宣言的スキーマ管理 o o
ドライラン --dry-run(CI未統合) schema apply --dry-run(GitHub Action対応)
破壊的変更の自動検出 なし migrate lint でDROP/RENAME等を自動検出・CI失敗
DDL前後のGo処理 なし フック機能でGoコード実行可能
ロールバック なし(ロールフォワードのみ) down migration自動生成
既存migrate.sqlからの移行 - そのまま desired state として利用可能
## 具体的にどう良くなるか
### ケース:カラムの型変更(INT → BIGINT)
現状:PRのdiffで型が変わったことはわかるが、本番で実行されるALTER文やテーブルロックの影響はデプロイするまでわからない。
Atlas導入後:CIが以下をPRコメントに自動投稿する
code:sql
-- Planned Changes:
ALTER TABLE orders MODIFY COLUMN price bigint NOT NULL;
-- ⚠️ This change requires a table lock on orders (estimated 1.2M rows)
→ レビュアーが「メンテナンス時間帯にデプロイしよう」と判断できる
### ケース:カラム名のtypo修正
現状:migrate.sql の diff は adress → address の変更に見える。mysqldef は DROP + ADD として実行するがレビューからは読み取りにくい。
Atlas導入後: migrate lint が以下を検出してCIを失敗させる
code:_
Destructive change detected:
DROP COLUMN adress on table users will delete all data in this column
→ 安全な手順(新カラム追加 → データコピー → 旧カラム削除)に修正できる
ケース: 正規化のためのテーブル分離
現状:2つ以上のPRに分けて手動で順番にデプロイ。手順書を書いてオペレーションする必要がある。
Atlas導入後:1つのPR内で手続き的マイグレーションを記述可能
code:_
migration_001_add_column.sql -- DDL
migration_002_migrate.go -- Goでデータ移行
migration_003_finalize.sql -- 制約追加・旧カラム削除
## 参考