【Laravel】マイグレーションの書き方完全ガイド|カラム型一覧・インデックス・外部キー設定

Laravel で機能を追加するとき、「どのカラム型を選ぶべきか」「インデックスはどこで貼るのか」「外部キーは foreignIdconstrained だけでよいのか」で迷う場面があります。

マイグレーションは、データベースの構造を PHP のコードとして管理し、チームで同じスキーマ変更を共有するための仕組みです。Laravel 公式ドキュメントでは「データベースのバージョン管理のようなもの」と説明されています。

はじめに

この記事では、Laravel のマイグレーションについて次の範囲を整理します。

  • マイグレーションファイルの作成・実行・ロールバック
  • よく使うカラム型とカラム修飾子
  • インデックスの貼り方
  • 外部キー制約の設定
  • 実務で起きやすい失敗例

前提と検証範囲

検証範囲は、2026-05-12 時点で確認した Laravel 12.x の Migrations 公式ドキュメントです。実際の挙動は Laravel のメジャーバージョンや利用するデータベース(MySQL、PostgreSQL、SQLite など)で変わる場合があります。手元の composer.json と接続先データベースに合わせて確認してください。

この記事の前提は、Laravel プロジェクトが作成済みで、ローカル DB に接続できる状態です。ターミナルで php artisan を実行できる環境を想定します。

用語も短く確認しておきます。Artisan は Laravel のコマンドラインツールです。スキーマはテーブルやカラムなどの構造を指します。ロールバックは、実行済みのマイグレーションを戻す操作です。

マイグレーションの基本

ファイルを作成する

マイグレーションは Artisan コマンドで作成します。

php artisan make:migration create_posts_table

生成されたファイルは database/migrations に置かれます。ファイル名にはタイムスタンプが付き、Laravel はその順番でマイグレーションを実行します。

create_posts_table のように名前からテーブル作成だと推測できる場合、Laravel は Schema::create を含む雛形を用意します。推測できない名前にした場合は、自分で対象テーブルを書きます。

updown の役割

マイグレーションには基本的に updown があります。

メソッド役割
upテーブル作成、カラム追加、インデックス追加など、前に進める変更を書く
downup で行った変更を戻す処理を書く

たとえば posts テーブルを作る最小例は次のようになります。

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

down を書いておくと、開発中にロールバックしてスキーマを戻しやすくなります。ただし本番環境でロールバックするかどうかは、データの削除や制約変更の影響を見て判断します。

実行・確認・ロールバック

よく使うコマンドは次のとおりです。

コマンド用途
php artisan migrate未実行のマイグレーションを実行する
php artisan migrate:status実行済み・未実行の状態を確認する
php artisan migrate --pretend実行予定の SQL を表示する
php artisan migrate:rollback直近バッチを戻す
php artisan migrate:rollback --step=1直近 1 件だけ戻す
php artisan migrate --force本番環境などの確認プロンプトを省いて実行する

--force は CI/CD や本番デプロイで使われますが、破壊的な変更も実行されます。実行前に --pretend、レビュー、バックアップ方針を確認しておくと事故を減らせます。

ローカルで試すだけなら、次の順番が分かりやすいです。

  1. php artisan make:migration create_posts_table でファイルを作る。
  2. database/migrations のファイルを編集する。
  3. php artisan migrate --pretend で SQL を確認する。
  4. php artisan migrate で実行する。
  5. php artisan migrate:status で状態を確認する。
  6. 必要なら php artisan migrate:rollback --step=1 で戻す。

カラム型一覧:まず押さえる型

Laravel のスキーマビルダーには多くのカラム型があります。ここでは実務でよく使うものを中心にまとめます。公式ドキュメントの完全な一覧は Available Column Types を参照してください。

メソッド用途の例
$table->id();自動採番の主キー
$table->foreignId('user_id');外部キー用の符号なし BIGINT 相当のカラム
$table->string('title', 255);短い文字列
$table->text('body');長い文章
$table->boolean('is_published');真偽値
$table->integer('sort_order');整数
$table->decimal('price', total: 8, places: 2);金額など桁数を指定した小数
$table->date('published_on');日付
$table->dateTime('published_at');日時
$table->timestamps();created_atupdated_at
$table->softDeletes();論理削除用の deleted_at
$table->json('metadata');JSON データ
$table->uuid('uuid');UUID
$table->ulid('ulid');ULID
$table->enum('status', ['draft', 'published']);限られた文字列の候補

選び方の目安も押さえておきます。短い名前やタイトルは string、本文や説明文は text が候補になります。日付だけなら date、時刻まで必要なら dateTimetimestamp を使います。構造が固定されていない補足情報は json が候補になりますが、検索条件に使う値は通常のカラムとして切り出すほうが扱いやすいです。

enum は便利ですが、候補の変更が運用上の負担になる場合があります。ステータスの選択肢が増減しやすいなら、アプリケーション側のバリデーションや別テーブルで管理する設計も検討します。

カラム修飾子:型に条件を足す

カラム型の後ろには、nullabledefault などの修飾子をつなげられます。

Schema::table('posts', function (Blueprint $table) {
    $table->string('summary', 160)->nullable();
    $table->boolean('is_published')->default(false);
    $table->timestamp('published_at')->nullable();
});

よく使う修飾子は次のとおりです。

修飾子意味
nullable()NULL を許可する
default($value)デフォルト値を設定する
comment('...')カラムコメントを付ける
after('column')MySQL で指定カラムの後ろに配置する
change()既存カラムを変更する

既存カラムの変更はデータベースによって制約があります。特に本番テーブルでは、カラム変更がロックや長時間実行につながる場合があるため、事前にステージング環境で実行時間と SQL を確認します。

実装例:記事テーブルを作る

ここまでの要素を合わせると、ブログ記事のような posts テーブルは次のように表現できます。

この例は、外部キーとインデックスも含めた実務寄りの形です。users テーブルはすでに存在する前提で読み進めてください。Laravel の標準的な認証機能を使うプロジェクトでは、users テーブルが用意されている構成がよくあります。

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('body');
            $table->string('status')->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->json('metadata')->nullable();
            $table->timestamps();

            $table->index(['status', 'published_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

この例では、次の設計を表しています。

  • user_idusers テーブルの id を参照する。
  • ユーザーが削除されたとき、紐づく記事も削除する。
  • slug は重複させない。
  • 公開状態と公開日時で絞り込む想定があるため、複合インデックスを貼る。

cascadeOnDelete は親レコード削除時に子レコードも削除します。実務では「ユーザー削除時に記事も消してよいか」を仕様として確認してから使います。

インデックスの書き方

インデックスは検索や一意制約のために使います。Laravel ではカラム定義に続けて書く方法と、後からインデックスだけ書く方法があります。

たとえば公開済み記事の一覧で status = 'published' を条件にし、published_at の降順で並べる画面があるとします。この場合、statuspublished_at の複合インデックスを検討できます。

Schema::table('users', function (Blueprint $table) {
    $table->string('email')->unique();
    $table->index('created_at');
    $table->index(['account_id', 'created_at']);
});

主なメソッドは次のとおりです。

メソッド用途
$table->primary('id');主キー
$table->primary(['id', 'parent_id']);複合主キー
$table->unique('email');一意インデックス
$table->index('state');通常のインデックス
$table->fullText('body');全文インデックス。対応はデータベースに依存する

インデックスは多ければよいわけではありません。読み取りは速くなる可能性がありますが、書き込み時には更新対象が増えます。まずは検索条件、並び替え、ユニーク制約として必要な場所に絞って貼るのが現実的です。

外部キー制約の書き方

外部キー制約は、関連する親レコードが存在することをデータベース側で保証する仕組みです。

usersposts の関係では、対応は次のようになります。

要素
親テーブルusers
子テーブルposts
子テーブル側の外部キーposts.user_id
参照先users.id

古い書き方に近い形では、カラム作成と参照先を分けて書けます。

Schema::table('posts', function (Blueprint $table) {
    $table->unsignedBigInteger('user_id');

    $table->foreign('user_id')
        ->references('id')
        ->on('users');
});

Laravel では、規約に沿う場合は foreignIdconstrained で短く書けます。

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained();
});

foreignId('user_id') は、符号なし BIGINT 相当のカラムを作ります。constrained()user_id という名前から users テーブルの id を参照すると推測します。

テーブル名が規約と違う場合は、参照先を明示します。

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('author_id')
        ->constrained(table: 'users');
});

削除・更新時の挙動も指定できます。

$table->foreignId('user_id')
    ->constrained()
    ->cascadeOnUpdate()
    ->restrictOnDelete();

代表的な指定は次のとおりです。

メソッド意味
cascadeOnDelete()親が削除されたら子も削除する
restrictOnDelete()子がある親の削除を制限する
nullOnDelete()親が削除されたら外部キーを NULL にする
cascadeOnUpdate()親キー更新を子にも反映する
restrictOnUpdate()子がある親キー更新を制限する

nullable などのカラム修飾子は、公式ドキュメントの説明どおり constrained より前に書きます。

$table->foreignId('user_id')
    ->nullable()
    ->constrained();

実務でのチェックリスト

ここまでの内容を読みながら、次の観点を確認するとレビューしやすくなります。

  • カラム名と型は、保存する値の意味に合っているか。
  • nullabledefault の方針は、アプリケーションの入力仕様と合っているか。
  • 検索条件や一意制約として必要な場所にだけインデックスを貼っているか。
  • 外部キーの削除・更新時の挙動は仕様と合っているか。
  • down は開発環境で戻せる内容になっているか。
  • 本番で実行する前に migrate --pretend やステージング環境で確認しているか。

よくある失敗例

失敗例:親テーブルより先に外部キー付きテーブルを作る

posts.user_idusers.id を参照する場合、users テーブルが存在しない状態で posts テーブルを作ると、外部キー制約の作成に失敗します。

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->timestamps();
});

このコード自体はよくある形です。ただし create_users_table より実行順が早いファイル名だと問題になります。マイグレーションファイル名のタイムスタンプを確認し、親テーブルから作成される順番へ変更します。

失敗例:外部キーの削除方針を決めずに cascadeOnDelete を使う

cascadeOnDelete は便利ですが、親レコードを消したときに子レコードも消えます。ユーザー退会時に投稿を残す仕様なら、nullOnDelete や論理削除など別の設計が向いている場合があります。

私も過去の案件で、退会ユーザーに紐づく投稿まで消えてしまう設計にしてしまったことがありました。原因は、削除方針を仕様として確認しないまま cascadeOnDelete を書いていたことです。データベースとしては正しく動いていても、業務上は「投稿は残すべきだった」という判断になるケースがあります。

外部キー制約は「データベースの整合性」だけでなく、「業務上どのデータを残すか」に関わります。コードを書く前に削除方針を決めておくと、後からの修正を減らせます。

失敗例:down が戻せない内容になっている

開発中に migrate:rollback --step=1 を使うなら、down の内容も確認します。たとえば upposts テーブルを作ったのに、down が空のままだとロールバックしてもテーブルは残ります。

public function down(): void
{
    Schema::dropIfExists('posts');
}

本番環境では、ロールバックよりも前方修正のマイグレーションを追加する判断もあります。どちらを選ぶかは、データ保持とリリース手順に合わせて決めます。

まとめ

Laravel のマイグレーションは、テーブル定義をチームで共有するための重要な仕組みです。まずは updown、カラム型、インデックス、外部キー制約の役割を分けて理解すると、レビューしやすいマイグレーションを書けます。

特に外部キーと削除方針は、コードの短さだけで決めないほうが安全です。constrainedcascadeOnDelete は便利ですが、参照先の規約、テーブル作成順、業務上のデータ保持方針とセットで確認します。

次に試せること

  1. ローカル環境の Laravel プロジェクトで php artisan make:migration create_posts_table を実行し、この記事の posts テーブル例を写経してみる。
  2. php artisan migrate --pretend で発行予定の SQL を確認する。
  3. 本番 DB ではなくローカル DB で、cascadeOnDeleterestrictOnDeletenullOnDelete の違いを試す。

Source