Laravel 12時代のマイグレーション整理術

2025.11.14 09:00
2025.10.22 09:50
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 1reorder INT NOT NULL DEFAULT 999label/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 時代”の強い運用だと思っています。

今回は以上です!