はじめに
後からマイグレーションツール(phinx 等)を入れて、既存スキーマも全部そのツールに書き直すべきか迷っている方へ。
マイグレーションツールを後から入れると、必ず一度は迷います。「じゃあ今まで手書きの .sql で積んできた既存スキーマも、全部このツールの migration に書き直すべきか?」
僕が運用しているサービス(自作フレームワーク NENE2 の上に載った広告配信系のバックエンド、nene-serve)でも、まさにこの分岐に来ました。既存スキーマは連番の生 SQL ファイルで 0001_create_organizations.sql … と積み上がっていて、その数ちょうど 33 本。ここに新しく phinx を配線するにあたって、「33 本を全部 phinx の PHP migration に移す」か「移さない」かを決める必要がありました。
結論から言うと、移しませんでした。33 本は生 SQL の baseline のまま据え置き、phinx は「これから増える分」だけを担当します。過去と未来で書式が違う状態を、負債として放置するのではなく 「境界」として README に明文化して確定させました。この記事は、その判断の理由と、.sql と .php が同じディレクトリで安全に共存できる仕組みを、実際の設定ファイル付きでまとめます。
この記事で分かること:
- 「これからツールを使う」と「過去も全部そのツールに揃える」を別の意思決定として分ける考え方
-
.sqlbaseline と.phpmigration を同じディレクトリで安全に共存させる phinx 設定 - やらなかった判断(33本を移さない/phinx を require に上げない)を README で境界として宣言する運用
前提: これまでの baseline は「生 SQL のループ適用」だった
phinx を入れる前、nene-serve のスキーマ適用はいたってシンプルでした。database/migrations/ に連番の .sql を置き、まっさらな DB にはそれを順番に流すだけです。
# CI (.github/workflows/ci.yml) がやっていること、の本質
for f in database/migrations/*.sql; do
mysql … < "$f"
done
この 33 本が、既存スキーマの **単一の正典(SSOT)**です。MySQL で書かれていて、PostgreSQL 向けには scripts/mysql-to-pgsql.php が *.sql を glob して DDL を変換します。つまり「生 SQL の baseline」という前提が、CI にも変換スクリプトにも染み込んでいます。
database/migrations/
├── 0001_create_organizations.sql
├── 0002_create_users.sql
├── 0003_create_placements.sql
│ … (中略)
├── 0032_create_assets.sql
└── 0033_add_custom_domain_to_organizations.sql ← 33 本目
動いています。CI もグリーン。PostgreSQL 変換も回っています。ここに手を入れる理由が「ツールを統一したいから」だけなら、それは触る理由として弱いです。
なぜ phinx を入れたのか(そして何のために入れなかったのか)
phinx を入れたい動機は「これから」にありました。新しいテーブルやカラムを足すたびに手書き SQL を積むより、up() / down() が書けて status で適用状況が見えて rollback もできるツールに乗りたい。sibling の NENE2 が既に phinx 規約を持っていたので、それに揃えます。
ここで大事なのは、「これから phinx にしたい」と「過去の 33 本も phinx にすべき」は別の問いだということです。前者は素直に yes。後者を一緒くたに yes にすると、動いている 33 本の SSOT を、機能的な利得ゼロのために全部書き換えることになります。
33 本を PHP migration に移すと何が起きるか、を具体的に並べると割に合わなさが見えてきます。
- リスクが大きいです。 既存スキーマの正典を丸ごと別書式に書き換える= SSOT の作り直しです。移し間違い(型・デフォルト・インデックス・外部キー)が 1 箇所でもあれば、既存 DB との差分になって表面化しにくいバグを生みます。
- 利得がありません。 既に本番も CI も生 SQL の baseline で回っています。移したところで「今動いているものが、別の書き方で動く」だけ。ユーザにも運用にも新しい価値は出ません。
-
PostgreSQL 変換系も道連れになります。
scripts/mysql-to-pgsql.phpは*.sql前提です。33 本を.phpにしたら、この変換パイプラインごと作り直しになります。
というわけで、移行は意図的に見送りました(deferred)。「今後は phinx、既存は生 SQL の baseline」という二層構成を、その場のノリではなく 設定ファイルと README に書いて確定させました。
仕組み: phinx は *.php しか見ない。だから .sql と共存できる
「同じディレクトリに .sql と .php が混ざったら phinx が誤爆しないか?」——ここが技術的な肝で、答えは しない、です。phinx はマイグレーションとして *.php しか走査しません。連番の .sql baseline は phinx から見て完全に不可視で、同じ database/migrations/ に置いたまま安全に共存します。
設定 phinx.php は、この方針をそのままコメントに書いてあります。接続情報はアプリと同じ NENE2 の ConfigLoader 経由なので、phinx は .env が指す DB をそのまま叩きます。
<?php
// phinx.php (抜粋)
$database = (new ConfigLoader(__DIR__))->load()->database;
return [
'paths' => [
'migrations' => 'database/migrations', // .sql baseline と同居
'seeds' => 'database/seeds',
],
'environments' => [
'default_environment' => $database->environment,
$database->environment => $database->usesUrl()
? ['url' => $database->url]
: [
'adapter' => $database->adapter,
'host' => $database->host,
'name' => $database->name,
'user' => $database->user,
'pass' => $database->password,
'port' => $database->port,
'charset' => $database->charset,
],
],
'version_order' => 'creation',
];
migrations パスを baseline と同じ database/migrations に向けているのがポイントです。ディレクトリを分けなくていい。phinx が .sql を無視してくれるので、「過去の 33 本」と「これから積む PHP migration」が 1 つのディレクトリに時系列で並びます。
コマンドは composer script に寄せました。
// composer.json (抜粋)
"scripts": {
"migrations:status": "phinx status -c phinx.php",
"migrations:migrate": "phinx migrate -c phinx.php",
"migrations:rollback": "phinx rollback -c phinx.php",
"migrations:create": "phinx create -c phinx.php",
"migrations:seed": "phinx seed:run -c phinx.php"
}
これで新しいスキーマ変更はこう積みます。
composer migrations:create -- CreateWidgetsTable # PHP migration を scaffold
composer migrations:status # 適用/未適用を一覧
composer migrations:migrate # 未適用を適用
composer migrations:rollback # 直近バッチを rollback
境界を「宣言」する: fresh DB のブートストラップ順
二層構成のいちばんの落とし穴は、次にこのコードを触る人が「baseline も phinx で管理されている」と誤解することです。誤解すると、migrations:status に 33 本が出てこないのを見て「マイグレーションが欠けている」と勘違いしたり、baseline を phinx 化しようとして SSOT を壊したりします。
だから境界は、コードの挙動として存在するだけでは足りません。言語化して README に置きます。nene-serve では database/README.md に、ブートストラップの順序と各層の役割をこう固定しました。
-
生 SQL の baseline をループ適用します(前述の
for f in …)。これで DB が「出荷時点のスキーマ」になります。 -
composer migrations:migrateを回します。baseline 以降に phinx で足した分だけが適用されます。
phinx は自分が適用した migration だけを phinxlog テーブルに記録します。baseline は phinx の管理対象ではなく、一度きりのブートストラップであって、phinxlog には載りません。この非対称を README に書いておかないと、必ず誰かがハマります。
そして「33 本を phinx に移さなかった」ことも、判断として README に明記しました。「大きいし、機能的利得なしに SSOT を危険にさらすので、意図的にやらなかった」——こう書いておけば、半年後の自分や次の担当が「なぜ揃っていないんだ、揃えよう」と善意で地雷を踏むのを防げます。やらなかったことの理由こそ、コメントに残す価値があります。
積み残し: phinx を require に上げるかは「その時」まで保留
正直に deferred をもう一つ書いておきます。robmorgan/phinx は今 require-dev に入れてあります。本番を composer install --no-dev で作ると phinx が入らないので、本番デプロイで composer migrations:migrate を直接回す運用になったら、phinx を require に移す必要があります。
今はそうなっていません。本番のスキーマ適用は生 SQL のループなので、phinx は開発時にしか要らず require-dev で足ります。だから移していません。これも「今やらない理由」を README の注意書きに残して、必要になった瞬間に気づけるようにしました。この手の「将来必要になるかもしれない移動」は、先回りしてやると使いもしない依存を本番に積むことになります。トリガー条件を書いて保留するのが、いちばん安いです。
検証したこと
配線が本当に無害かは、機能を実際に動かして確かめました。
- phinx を sqlite で
create → migrate → status(up) → rollbackまで一通り実機で回しました(検証用の temp migration は確認後に削除)。 -
.sqlbaseline が phinx の走査対象外であることを実際に確認しました。33 本の.sqlはmigrations:statusに一切現れません。 -
composer checkはグリーン(PHPUnit / PHPStan level 8 / php-cs-fixer clean)。既存の生 SQL 適用も PostgreSQL 変換も挙動不変です。
「設定を足しただけ、既存には触っていない」を口で言うだけでなく、baseline が phinx から見えないことを目視で確かめてから確定させました。
学び
- 「これからツールを使う」と「過去も全部そのツールに揃える」は、別の意思決定として分けて判断します。 前者に yes でも、後者は「動いている SSOT を利得なしに作り直す」コストを別途量ります。まとめて yes にしません。
-
混在は、宣言すれば負債になりません。
.sqlの過去と.phpの未来が同居していても、境界(どっちが何を担当し、どういう順で適用されるか)を README とコメントで明文化すれば、それは「未整理の負債」ではなく「意図された構成」になります。 -
ツールの走査規則を味方につけます。 phinx が
*.phpしか見ないという性質のおかげで、ディレクトリを分けずに共存できました。ツールの「見ないもの」を理解すると、無理な移行をせずに境界を引けます。 - やらなかったことにこそ理由を書きます。 「33 本を移さなかった」「phinx を require に上げなかった」は、コードには現れない判断です。README に理由とトリガー条件を残しておかないと、次の人が善意で蒸し返します。
マイグレーションツールを後入れするとき、「既存も全部移すべきか」で手が止まっているなら、一度「移さないと何が困るか」を具体的に書き出してみてほしいです。困らないなら、境界を宣言して据え置くのが、いちばん安全で安いです。
一次資料
- nene-serve PR #141
chore(test,db): adopt NENE2 DatabaseTestKit + wire phinx for future migrations(Closes #140) - 設定:
phinx.php(NENE2ConfigLoader経由・migrationsパスは baseline と同居)/composer.jsonのmigrations:*scripts(robmorgan/phinx ^0.16.11・現状 require-dev) - 運用契約:
database/README.md(baseline=生 SQL の SSOT・不変/今後= phinx/fresh DB のブートストラップ順/require-dev 保留の注意書き) - 変換系:
scripts/mysql-to-pgsql.php(*.sqlのみ glob・phinx PHP migration には触れない)
シリーズ/相互リンク(いずれも執筆中・公開前)
- 運用系: (DatabaseTestKit のテスト配線統一 / nene-serve #141 の同 PR)、(1 ホスト多スタックのポート設計)。
── 筆者: 森 秀之(彩音インターナショナル)— 自己ホストの業務ツール群を実運用中。
中小企業向けの業務システムを料金公開・固定価格で開発しています。
🔗 ayane.co.jp