Laravel 12時代のマイグレーション整理術
なんとなく増やしてきた create_xxx_table 達。
後から見ると「意図」「命名」「制約」がバラついてて、直すのが怖い…。
そこで、最初から“決め切る”ための小さなルールを用意しました。
目次
先に結論(5つだけ覚える)
1 NULL禁止・既定値主義
基本は NOT NULL + DEFAULT。曖昧さを消すのが最強。
2 外部キーは「更新CASCADE・削除RESTRICT」がデフォ
安易な cascadeOnDelete() は事故の元。必要なテーブルだけ明示で許可。
3 索引命名を固定(探せることが正義)
idx_{table}_{col} / uq_{table}_{col} / fk_{table}_{col}。
4 コメントで“ねらい”を残す
未来の自分(とチーム)へのメモ。行頭の区切りで目に付くように。
5 辞書テーブル/フラグ/並び順の“定番カラム”を揃える
display TINYINT(1) NOT NULL DEFAULT 1、reorder INT NOT NULL DEFAULT 999、label/serial など。
フラグ・小さな辞書を“先に決めておく”
- 表示フラグ:
display TINYINT(1) NOT NULL DEFAULT 1 - 並び順:
reorder INT NOT NULL DEFAULT 999(“末尾”の意味を数字で固定) - 辞書テーブル(カテゴリ等)は id/name/label/serial/reorder/display を最初から入れる
- ENUMは避ける(MySQL依存が強くなる)→ TINYINT + 辞書 で表現
Schema::create('spot_categories', function (Blueprint $t) {
$t->id();
$t->string('name', 100);
$t->string('label', 50)->default('');
$t->string('serial', 50)->default('');
$t->unsignedInteger('sort_order')->default(999);
$t->unsignedTinyInteger('display')->default(1);
$t->timestamps();
$t->unique('serial', 'uq_spot_categories_serial');
});
外部キーの“基本姿勢”
- 更新は CASCADE(親のIDが変わることは稀だが安全側)
- 削除は RESTRICT(勝手に子が消えると事故る)
- ほんとうに連鎖削除したい箇所だけ、明示で
cascadeOnDelete()。
運用でよく壊れるのは「消したら芋づるで消えた」やつ。
禁止がデフォルト、許可は例外に。複合ユニーク・検索用索引の“型”
複合ユニーク:
uq_{table}_{col1}_{col2}検索用インデックス:使う順にカラムを並べる(WHERE順を意識)
部分一致 LIKE 対策:左前方一致に寄せる(
where('name', 'like', "{$prefix}%"))。
フリーワード全文検索はアプリ側で N-gram / Meilisearch 等を検討。
$table->unique(['company_id', 'serial'], 'uq_spots_company_serial');
$table->index(['display', 'reorder', 'id'], 'idx_spots_list'); // 一覧用ピボット/中間テーブルの型
- テーブル名は
spot_tagなど 単数_単数(慣習どおりでOK) - カラムは 両方のFKのみ+必要なら
display/reorder - 複合ユニークで重複を禁止
Schema::create('spot_tag', function (Blueprint $t) {
$t->unsignedBigInteger('spot_id');
$t->unsignedBigInteger('tag_id');
$t->unique(['spot_id', 'tag_id'], 'uq_spot_tag_pair');
$t->foreign('spot_id', 'fk_spot_tag_spot')
->references('id')->on('spots')
->cascadeOnUpdate()->cascadeOnDelete();
$t->foreign('tag_id', 'fk_spot_tag_tag')
->references('id')->on('tags')
->cascadeOnUpdate()->restrictOnDelete();
});
“安全な変更系”の作法(後方互換)
追加は安全:nullable(false) にしたい列は ①追加時はDEFAULT付き→②データ移行→③DEFAULT/NOT NULL確定の3段階。
リネームは慎重:renameColumn は環境によってロックが重い。新列追加→移行→旧列削除の分割が無難。
既存列の型変更は避けて**“新列を正”**に切り替える。
タイムスタンプ&メタ情報の統一コメント
// ==================================================
// メタ情報(状態・履歴)
// ==================================================
/**
* 登録日時(created_at)/更新日時(updated_at)
* Laravel の timestamps() により、Eloquent が自動更新
*/
$table->timestamps();Seed・辞書の“セット運用”
- 辞書テーブルは Seeder 同梱(id固定 or serial固定)
- 参照側では 辞書の id/serial をコードに直書きしない(リポジトリ or 辞書クラス経由)
- テストは Stub 辞書 or Factory で十分(本番辞書に依存しない)
仕上げチェックリスト
- NULL禁止+既定値主義で書けている
- 外部キーは 更新CASCADE・削除RESTRICT がデフォルト
- 索引の命名が
idx_ / uq_ / fk_で統一 -
display / reorder / serial / labelの定番カラムが揃っている - 変更系は 分割(追加→移行→確定) 戦略で書けている
- コメントに “目的/設計メモ/履歴” を残した
まとめ
マイグレーションは“正解”より“一貫”が効きます。
NULLを捨てて既定値に寄せる、外部キーは削除RESTRICTが基本、
索引の命名とコメントを固定——これだけで、未来の自分がだいぶ救われます。
あとはテーブルを開いたときに、何を意図したのかが1分で伝わること。
それが“Laravel 12 時代”の強い運用だと思っています。
今回は以上です!