2026年6月3日

2026年6月3日

Laravelのマイグレーションの書き方と実行:テーブル作成・カラム変更・本番での安全な流し方

Laravel のマイグレーションは、データベースのテーブル構造を PHP のコードとして書き、php artisan migrate で順番に適用する仕組みです。構造の変更履歴を Git で管理でき、開発環境・ステージング・本番で同じ構造を再現できます。この記事では、マイグレーションファイルの作り方と書き方、カラムの追加と変更、実行と巻き戻しのコマンド、本番で流すときの注意点、よくあるエラーの対処までを説明します。

マイグレーションの仕組み

マイグレーションファイルは database/migrations に置かれ、ファイル名の先頭に日時が付きます。php artisan migrate を実行すると、Laravel はデータベースの migrations テーブルを見て、まだ実行していないファイルだけを日時の順に実行し、実行済みとして記録します。

各ファイルには2つのメソッドがあります。

  • up(): 変更を適用する処理(テーブルを作る、カラムを足すなど)
  • down(): その変更を取り消す処理(migrate:rollback で使われる)

down() は、up() をちょうど打ち消す内容にしておきます。これが崩れていると、巻き戻したときにテーブルが中途半端に残ります。

接続先のデータベースは .env の DB_CONNECTION などで決まります。新しい版の Laravel では、新規プロジェクトの既定が SQLite になっている場合があるため(2026年10月現在)、MySQL を使うなら .env の設定を先に確認してください。

テーブルを作るマイグレーション

ファイルの作成

php artisan make:migration create_posts_table

名前を create_〇〇_table の形にすると、テーブル作成用のひな形が生成されます。中身を次のように書きます。

<?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->text('body');
            $table->string('status', 20)->default('draft')->index();
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

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

return new class extends Migration という無名クラスの書き方は、現在の Laravel の標準です。古いプロジェクトでは class CreatePostsTable extends Migration のようにクラス名が付いていますが、動作は同じです。

カラム定義のポイント

  • id(): 自動採番の主キー(符号なし BIGINT)を作ります。
  • foreignId('user_id')->constrained(): users テーブルの id を参照する外部キーを作ります。参照先のテーブル名はカラム名から推測されます。cascadeOnDelete() を付けると、ユーザーを削除したときにその投稿も削除されます。消えては困るデータなら、付けずに削除を拒否させるか、nullOnDelete() を検討します。
  • string('status', 20): 長さを指定した VARCHAR です。省略すると既定の長さになります。
  • nullable(): NULL を許可します。後から追加するカラムは、既存の行に値がないため、nullable() か default() が必要になることが多いです。
  • index(): 検索や並べ替えに使うカラムには索引を付けます。何にでも付けると書き込みが遅くなるので、実際の検索条件に合わせます。
  • timestamps(): created_at と updated_at を作ります。Eloquent はこの2つを自動で更新します。
  • softDeletes(): deleted_at を作り、論理削除に使います。

既存テーブルを変更するマイグレーション

カラムを追加する

php artisan make:migration add_slug_to_posts_table --table=posts
public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->string('slug')->nullable()->unique()->after('title');
    });
}

public function down(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->dropUnique(['slug']);
        $table->dropColumn('slug');
    });
}

after() はカラムの並び順を指定するもので、MySQL と MariaDB で使えます。down() では、索引を先に削除してからカラムを削除しています。

既存の行がある状態で、いきなり NOT NULL かつ一意のカラムを追加すると、既存の行に入る値がなくエラーになります。まず nullable() で追加し、既存の行に値を入れてから、別のマイグレーションで NOT NULL に変える、という段階を踏むと安全です。

カラムを変更する

Schema::table('posts', function (Blueprint $table) {
    $table->string('title', 500)->nullable()->change();
});

change() は、カラムの型や長さ、NULL 許可を変更します。注意点は、新しい版の Laravel では、変更後も残したい属性(nullable()、default()、comment() など)をすべて書き直す必要があることです。書かなかった属性は外れます。古い版(Laravel 10 より前)では change() を使うために doctrine/dbal パッケージが必要でした。挙動は版によって違うので、プロジェクトの Laravel の版に合わせて公式ドキュメントのアップグレードガイドを確認してください。

マイグレーションでデータを触るとき

既存の行に値を入れるなど、データの変更をマイグレーションで行う場合は、Eloquent のモデルではなく DB::table('posts')->... のようなクエリビルダーを使うのが無難です。モデルはこの先も変更されるため、半年後に新しい環境でマイグレーションを最初から流したとき、当時と違う動きをすることがあります。件数が多い場合は chunkById() で少しずつ処理します。

実行と巻き戻しのコマンド

php artisan migrate:status      # 実行済み・未実行の一覧
php artisan migrate --pretend   # 実行される SQL を表示するだけ(実行しない)
php artisan migrate             # 未実行のものを実行
php artisan migrate:rollback    # 直前のバッチを取り消す
php artisan migrate:rollback --step=1   # 直近のファイル1つだけ取り消す

migrate は1回の実行で流したファイルをまとめて1つの「バッチ」として記録し、rollback はそのバッチ単位で戻します。--pretend で事前に SQL を確認する習慣を付けると、意図しない削除や型変更に気づけます。

次の2つは、開発環境専用と考えてください。

  • migrate:refresh: すべてを巻き戻してから、もう一度実行します。
  • migrate:fresh: すべてのテーブルを削除してから実行します。down() は使われません。

どちらもデータが消えます。.env の接続先が本番を向いたまま実行する事故を防ぐため、作業前に php artisan about(環境の確認)や php artisan db:show(接続先データベースの確認)で、どこに接続しているかを確かめてください。

本番で流すときの注意点

本番環境(APP_ENV=production)では、migrate を実行すると確認の質問が表示されます。デプロイの自動化では --force を付けて質問を省略します。

php artisan migrate --force

本番で流す前に、次の点を確認します。

  1. バックアップを取る: データベースのダンプかスナップショットを取得し、戻せる状態にしてから実行します。
  2. ステージングで同じ手順を試す: 本番に近いデータ量で実行時間とエラーの有無を確認します。
  3. 実行済みのファイルは書き換えない: 本番で実行済みのマイグレーションを編集しても、再実行されません。修正は新しいマイグレーションとして追加します。
  4. 大きなテーブルの変更に注意する: 行数の多いテーブルのカラム変更や索引追加は、データベースによっては長時間かかり、その間の書き込みが待たされることがあります。アクセスの少ない時間帯に行うか、MySQL のオンライン DDL の挙動を確認し、必要なら専用のツールの利用も検討します。
  5. アプリとの順序を考える: カラムを削除する場合、先にコードからそのカラムの参照をなくしてデプロイし、その後でカラムを削除するマイグレーションを流すと、デプロイの途中でエラーが出にくくなります。

複数のサーバーに同時にデプロイする構成では、migrate --isolated を使うと、同時に複数のマイグレーションが走らないようにロックをかけられます。ロックにはキャッシュを使うため、サーバー間で共有できるキャッシュ(Redis やデータベースなど)を設定しておく必要があります。

マイグレーションのファイルが増えすぎたら、php artisan schema:dump で現在の構造を SQL ファイルにまとめる方法もあります。新しい環境では、まずその SQL を読み込み、残りのマイグレーションだけを実行します。

よくあるエラーと確認方法

  • 「Specified key was too long」: 古い MySQL や MariaDB で、utf8mb4 の文字列カラムに索引を付けたときに出ます。データベースを新しい版に更新するのが本来の対処で、すぐに難しい場合は AppServiceProvider の boot() で Schema::defaultStringLength(191); を指定する回避策があります。
  • 外部キーの作成に失敗する: 参照先のテーブルがまだ作られていない(ファイル名の日時の順序が逆)か、型が一致していない(id() は符号なし BIGINT なので、参照側は foreignId() を使う)ことが多い原因です。
  • 「Table already exists」: 手作業で作ったテーブルがある、または途中で失敗したマイグレーションが記録されないままテーブルだけ残った状態です。migrate:status と実際のテーブルを見比べ、残ったテーブルの扱いを決めてから再実行します。
  • rollback でエラーになる: down() が up() と対応していないことが原因です。ステージングで migrate と rollback を往復させて確認すると、本番で困る前に気づけます。

まとめ

  • マイグレーションは up() と down() を対にして書き、未実行のものだけが日時順に実行されます。
  • 既存の行があるテーブルへのカラム追加は、nullable() で追加してから段階的に制約を強めます。
  • change() は、残したい属性をすべて書き直します。版による違いは公式ドキュメントで確認します。
  • 実行前は migrate:status と --pretend で確認し、migrate:fresh などデータが消えるコマンドは開発環境に限ります。
  • 本番ではバックアップ、ステージングでの確認、実行済みファイルを書き換えない、の3点を守り、--force で実行します。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲