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