Laravel

Laravelマイグレーションの使い方|php artisan migrateとrollback・freshの挙動を13.xで検証

Laravelマイグレーションの使い方|php artisan migrateとrollback・freshの挙動を13.xで検証

Laravelのマイグレーションは、テーブル定義をPHPのクラスとしてリポジトリに置き、php artisan migrate で各環境のデータベースへ同じ変更を適用する仕組みです。この記事では Laravel 13.31.0(PHP 8.5.8・SQLite)で各コマンドを実行し、出力と終了コードを確認した結果をもとに、戻る範囲の決まり方と本番での事故の防ぎ方を説明します。

Laravel 13 の対応PHPバージョンやサポート期限は、Laravelの最新バージョンは13|対応PHPバージョン・サポート期限一覧とアップグレード判断で扱っています。

まとめ:Laravelマイグレーションで挙動を取り違えやすい7点

  • migrate:rollback を引数なしで実行すると、最後のバッチに含まれるファイルがすべて戻ります。1ファイルだけ戻すなら --step=1 を付けます。
  • migrate 側の --step は値を取りません。--step=1 と書くとエラーになり、終了コードは1です。
  • migrate:refresh は各ファイルの down() を順に実行するので、down() が壊れていると途中で止まります。migrate:fresh は down() を呼ばずに全テーブルを削除します。
  • APP_ENV=production で --force を付けずに非対話実行すると「Command cancelled.」で終了コード1になり、デプロイが失敗します。
  • DB::prohibitDestructiveCommands($this->app->isProduction()) と設定すると、本番では fresh・refresh・rollback・reset・db:wipe が --force 付きでも拒否され、migrate だけが通ります。
  • change() は書かなかった修飾子を落とします。nullable と default が消えることを確認しました。
  • SQLite と MySQL ではマイグレーションがトランザクションに包まれません。途中で失敗すると作成済みのテーブルが残り、再実行が「already exists」で失敗します。

以下、ファイルの作り方から順に、実行結果を添えて説明します。

マイグレーションファイルとmigrationsテーブルの役割

DBマイグレーションは、データベースの構造変更を手順としてコード化し、順番と実行済みかどうかを記録しながら適用する方法です。Laravelの公式ドキュメントは「データベースのバージョン管理のようなもの」と説明しており、チームの誰かが追加したカラムを各自が手作業で足す、という連絡を不要にするのが目的です。基幹システムを別基盤へ移す「システムのマイグレーション」とは別の意味です。テーブルやカラムの集合を指す「スキーマ」という語の整理は、データベースのスキーマとは?三層スキーマ(外部・概念・内部)の違いと設計・管理の実務を参照してください。

実行履歴を記録するmigrationsテーブルとバッチ番号

Laravelは実行済みのファイル名を migrations テーブルに記録します。テーブル名は config/database.php の 'migrations' => ['table' => 'migrations'] で決まり、列は id・migration・batch の3つです。1回の php artisan migrate で実行されたファイルには同じバッチ番号が付き、この番号がロールバックの単位になります。

ファイル名の先頭にあるタイムスタンプが実行順を決めます。Laravel 13 の新規プロジェクトには 0001_01_01_000000_create_users_table.php など3本が最初から入っており、日付の代わりに固定値を使って必ず先に実行されるようにしています。

make:migrationのファイル名で決まる雛形

php artisan make:migration は、ファイル名からテーブル名と「新規作成か変更か」を推測して雛形を作ります。13.31.0 で2本を生成すると、次の違いが出ました。

コマンドの名前部分 up() に入る雛形 down() に入る雛形
create_posts_table Schema::create(‘posts’) と id()・timestamps() Schema::dropIfExists(‘posts’)
add_status_to_posts_table Schema::table(‘posts’) と空のクロージャ Schema::table(‘posts’) と空のクロージャ

推測できない名前にすると、テーブル名は自分で書き込むことになります。生成先を変える --path はアプリのベースパスからの相対パスで指定します。ほかの make 系コマンドの生成先とオプションはphp artisan makeコマンド一覧|Laravel 13対応 生成先・オプション早見表に、ファイル名やテーブル名の付け方はLaravelの命名規則一覧|テーブル・モデル・コントローラーからコーディング規約までにまとめています。

Schemaファサードで書くテーブル作成とカラム追加

雛形の中身は Schema ファサードで書きます。up() に適用する変更、down() にその逆を書くのが基本形です。Laravel 13 の雛形はクラス名を持たない無名クラス(return new class extends Migration)で、同じクラス名の衝突が起きません。

<?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')->nullable()->default('untitled');
            $table->text('body');
            $table->timestamps();
        });
    }

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

既存テーブルへのカラム追加は Schema::table() で書き、down() では dropColumn() で消します。

public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->string('status', 20)->default('draft')->after('title');
    });
}

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

公式ドキュメントのカラム修飾子一覧では、after() は MariaDB と MySQL 専用です。SQLite で実行したときに生成されたSQLは alter table "posts" add column "status" varchar not null default 'draft' で、位置指定は付きませんでした。テーブルやカラムの有無で処理を分けたいときは Schema::hasTable('posts') と Schema::hasColumn('posts', 'status') が真偽値を返します。enum 型のカラム定義と値の追加はLaravelのmigrationでenumを扱う実践ガイド|カラム定義・値の追加・Eloquent連携【Laravel 13対応】で扱っています。

php artisan migrateの実行とmigrate:statusでの確認

php artisan migrate は、migrations テーブルに記録が無いファイルを古い順に実行します。初回は記録用のテーブルを作ってから実行に入ります。

$ php artisan migrate

   INFO  Preparing database.

  Creating migration table .............................. 7.16ms DONE

   INFO  Running migrations.

  0001_01_01_000000_create_users_table ................. 29.14ms DONE
  0001_01_01_000001_create_cache_table .................. 9.53ms DONE
  0001_01_01_000002_create_jobs_table .................. 13.28ms DONE

実行済みかどうかは php artisan migrate:status で確認します。角括弧の数字がバッチ番号で、未実行のファイルは Pending と表示されます。

$ php artisan migrate:status

  Migration name .............................................. Batch / Status
  0001_01_01_000000_create_users_table ............................... [1] Ran
  0001_01_01_000001_create_cache_table ............................... [1] Ran
  0001_01_01_000002_create_jobs_table ................................ [1] Ran
  2026_09_14_093332_create_posts_table ............................... [2] Ran
  2026_09_14_093333_add_status_to_posts_table ........................ Pending

--pending を付けると Pending の行だけに絞れます。--pending=1 と値を付けた場合は、未実行のファイルがあるとその値が終了コードになりました(未実行ありで1、無しで0)。デプロイ前に「適用漏れがあれば止める」チェックをCIに入れるならこの形が使えます。

実行前にSQLだけを表示するpretendオプション

php artisan migrate --pretend は、対象接続のマイグレーションSQLを実行せずに表示します。ただし、履歴テーブルが未作成なら作成されます。また、マイグレーション内のPHP処理自体は動くため、ファイル操作や外部API呼び出しなどの副作用まで防ぐ機能ではありません。本番へ流す前に、生成されるSQLがDBの方言でどうなるかを確認する用途です。

$ php artisan migrate --pretend

   INFO  Running migrations.

  2026_09_14_093332_create_posts_table ...........................................
  ⇂ create table "posts" ("id" integer primary key autoincrement not null, "user_id" integer not null, "title" varchar default 'untitled', "body" text not null, "created_at" datetime, "updated_at" datetime, foreign key("user_id") references "users"("id") on delete cascade)
  2026_09_14_093333_add_status_to_posts_table ....................................
  ⇂ alter table "posts" add column "status" varchar not null default 'draft'

migrateのstepオプションに値を付けたときのエラー

php artisan migrate --step は、今回実行するファイルに1本ずつ別のバッチ番号を付けます。あとで migrate:rollback を引数なしで実行しても、最後の1本だけが戻る状態になります。

このオプションはフラグなので、数値を付けると実行自体が拒否されます。ロールバック側の --step=1 と同じ感覚で書くと引っかかります。

$ php artisan migrate --step=1

  The "--step" option does not accept a value.

$ echo $?
1

migrate:rollbackで戻る範囲:最後のバッチ・step・batch

ロールバックは「どのファイルの down() を実行するか」の選び方が3通りあります。フレームワークの Migrator::getMigrationsForRollback() は、step が1以上ならその件数、batch が1以上ならそのバッチ、どちらも無ければ最後のバッチを選びます。

コマンド 戻る範囲 実行結果(バッチ2に2本がある状態)
migrate:rollback 最後のバッチ全体 2本とも戻る
migrate:rollback --step 最後のバッチ全体 2本とも戻る
migrate:rollback --step=1 新しい順に1本 add_status_to_posts_table だけ戻る
migrate:rollback --step 1 新しい順に1本 add_status_to_posts_table だけ戻る
migrate:rollback --batch=1 バッチ1の全ファイル users・cache・jobs の3本が対象
migrate:reset 実行済みの全ファイル すべて戻る

値を付けない --step は件数指定になりません。「1本だけ戻したつもりが、同じバッチの2本目のテーブルまで消えた」という事故はこの形で起きます。--step 1 のように空白で区切っても1本だけ戻りました。

戻す前に --pretend を付けると、実行される drop 文を確認できます。--batch=1 --pretend では、雛形の3本だけで jobs・job_batches・failed_jobs・cache・cache_locks・users・password_reset_tokens・sessions の8テーブルを削除するSQLが並びました。1ファイルが複数テーブルを作っていることが、ここで初めて分かる場合があります。

migrate:refreshとmigrate:freshの違いと使い分け

どちらも「作り直してから全マイグレーションを流す」コマンドですが、壊し方が違います。refresh は migrate:reset と同じく各ファイルの down() を新しい順に実行し、fresh は内部で db:wipe を呼んで全テーブルを削除します。

項目 migrate:refresh migrate:fresh
削除の方法 各ファイルの down() を実行 down() を呼ばず全テーブル削除
down() が例外を投げたとき 終了コード1で停止 影響なく完了
範囲の指定 --step=5 は戻す範囲を直近5本に限定 不可
削除対象 各 down() に書いた対象 接頭辞に関係なく全テーブル
シーダーの実行 --seed --seed

差がはっきり出るのは down() が正しくないときです。down() で例外を投げるファイルを用意すると、refresh は最初のロールバックで止まり、テーブルは何も戻りませんでした。同じ状態で fresh を実行すると、全テーブルを削除して6本すべてを流し直し、全ファイルがバッチ1になりました。

$ php artisan migrate:refresh

   INFO  Rolling back migrations.

  2026_09_14_999998_create_tags_table .................... 0.44ms FAIL

   RuntimeException

  down() is not implemented

$ php artisan migrate:fresh

  Dropping all tables .................................. 26.05ms DONE

   INFO  Preparing database.
  ...
  2026_09_14_999998_create_tags_table .................... 1.68ms DONE

使い分けは単純です。開発中に down() が正しく書けているかも確かめたいなら refresh、手元のテーブル状態が崩れていて早く初期状態へ戻したいなら fresh を使います。公式ドキュメントは fresh が接頭辞に関係なく全テーブルを削除すると警告しており、他のアプリと共有しているデータベースでは使えません。既定では既定の接続だけが対象で、別の接続は --database=admin のように指定します。ファイル型SQLiteでは全テーブル削除時にビューも消えます。他のDBでビューも削除する場合は --drop-views、PostgreSQL の型も消すなら --drop-types を付けます。初期データの投入はLaravel Seederの使い方|Factory連携と本番投入してよいデータの線引きを参照してください。

本番環境でのmigrate実行:forceとprohibitDestructiveCommands

forceなしの本番非対話実行と「Command cancelled.」

マイグレーション系コマンドは ConfirmableTrait を通り、environment() が production のときだけ確認プロンプトを出します。確認の既定値は「いいえ」なので、CIやデプロイスクリプトのように対話できない環境では何も実行されずに終了します。migrate 自体もこの確認の対象です。

$ APP_ENV=production php artisan migrate -n

  APPLICATION IN PRODUCTION.

   WARN  Command cancelled.

$ echo $?
1

デプロイスクリプトでは php artisan migrate --force と書きます。--force は確認を飛ばすだけで、失敗時の巻き戻しや安全装置を足すものではありません。デプロイ時に合わせて流すキャッシュ系コマンドの順番はLaravelのキャッシュクリア|cache:clearとoptimize:clearの違いと本番デプロイの手順で説明しています。

破壊系コマンドだけを本番で禁止する設定

--force を付ければ確認は消えるので、デプロイ用のエイリアスやシェル履歴から migrate:fresh --force が本番で実行される余地は残ります。これを塞ぐのが DB::prohibitDestructiveCommands() です。AppServiceProvider の boot() に1行足します。

use Illuminate\Support\Facades\DB;

public function boot(): void
{
    DB::prohibitDestructiveCommands($this->app->isProduction());
}

この設定で APP_ENV=production にして各コマンドを実行した結果です。

コマンド 結果 終了コード
migrate:fresh --force 拒否 1
migrate:refresh --force 拒否 1
migrate:rollback --force 拒否 1
migrate:reset --force 拒否 1
db:wipe --force 拒否 1
migrate --force 実行される 0

拒否されたときの表示は「This command is prohibited from running in this environment.」です。migrate:rollback も止まる点は導入前に決めておくべきです。本番で戻す運用を想定しているチームには合いません。ただ、down() の多くは dropColumn() や dropIfExists() でデータごと消す処理なので、本番では rollback に頼らず、逆向きの変更を新しいマイグレーションとして追加する方が安全です。この設定を採用する場合は、障害復旧も新しいマイグレーションで行う手順を用意します。通常の migrate 内に書いた削除処理は止めないため、変更内容のレビューは別途必要です。

複数サーバーからの同時実行を防ぐisolatedオプション

複数台のサーバーそれぞれのデプロイ処理が migrate を呼ぶと、同じ変更を同時に流そうとします。php artisan migrate --isolated を付けると、Laravelはキャッシュドライバでアトミックロックを取ってから実行し、ロック中に起動した他の migrate は何もせずに成功の終了コードで終わります。

公式ドキュメントが挙げる対応ドライバは memcached・redis・dynamodb・database・file・array で、条件として「すべてのサーバーが同じキャッシュサーバーと通信していること」を求めています。file を各サーバーのローカル領域に保存する構成では、複数台の排他はできません。array はプロセス内に保持するため、同一サーバーでも別プロセス間の排他には使えません。新規プロジェクトの既定は CACHE_STORE=database です。初期マイグレーションで cache_locks テーブルを作成済みの状態なら、この設定のまま --isolated 付きの実行が通りました。何も実行していない空のデータベースでは「no such table: cache_locks」で終了コード1になるため、初回だけは --isolated を付けずに流すか、別のキャッシュドライバを指定します。

マイグレーション失敗時に起きることと対処

途中失敗で残る作成済みテーブル(SQLite・MySQL)

1つのファイルで「テーブル作成」と「別テーブルの変更」を続けて書き、後者が失敗するようにして SQLite で実行しました。migrate は FAIL で止まりましたが、先に作った comments テーブルは残り、再実行は次のエラーで失敗しました。

SQLSTATE[HY000]: General error: 1 table "comments" already exists

原因はフレームワークの実装にあります。Migrator::runMigration() は、スキーマ文法が supportsSchemaTransactions() を返し、かつマイグレーションの $withinTransaction が真のときだけトランザクションで包みます。13.x のソースで $transactions = true を持つのは PostgresGrammar と SqlServerGrammar で、MySQL・MariaDB・SQLite の文法には指定がありません。

MySQL や SQLite で運用するなら、1ファイルに入れる変更は1つにします。失敗したら、適用済みの変更と保存データを確認します。削除して再実行するのは、失敗した処理で新設され、保存すべきデータがない対象に限ります。本番ではバックアップと復旧手順を確認し、既存データを保持する方法で修復します。migrations テーブルには失敗したファイルの記録が残らないので、消した後の migrate は同じファイルを最初から実行します。

change()で消える書かなかった修飾子

既存カラムの変更は change() で書きます。公式ドキュメントは「残したい修飾子はすべて明示的に含める必要があり、書かなかった属性は削除される」と定めており、Laravel 11 のアップグレードガイドではこの変更を影響度 High に分類しています。title カラム(nullable()・default('untitled') 付き)に長さだけを指定して変更した結果です。

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

// 変更前: 'nullable' => true,  'default' => '\'untitled\''
// 変更後: 'nullable' => false, 'default' => NULL

長さだけを変えたつもりでも、NULL を許していたカラムが NOT NULL になり、既定値も消えます。正しくは $table->string('title', 100)->nullable()->default('untitled')->change(); と全部書き直します。なお Laravel 11 で Doctrine DBAL への依存が削除されており、13.31.0 の新規プロジェクトにも doctrine/dbal は入っていませんが、change() はそのまま動きました。

失敗しても終了コード0になるgracefulオプション

php artisan migrate --graceful は、マイグレーションが失敗しても成功の終了コードを返すオプションです。存在しないテーブルを変更するファイルで試すと、エラーは WARN として表示されたうえで終了コードは0でした。付けなければ同じ失敗で1になります。

CIやデプロイスクリプトは終了コードで次の工程へ進むかを判断するので、--graceful を付けると失敗したまま新しいコードが公開されます。テーブルが無くても起動を止めたくない一時的な環境以外では使いません。

実行済みファイルの編集・削除と実行記録

実行済みのファイルを書き換えても、migrate は「Nothing to migrate.」と表示して何もしません。記録があるファイルは実行済みとして扱われるので、変更は新しいマイグレーションとして作ります。

実行済みのファイルを削除した状態で migrate:rollback を実行すると、そのファイルの行に「Migration not found」と出て、他のファイルだけが戻りました。終了コードは0で、migrations テーブルには削除したファイルの記録が残ります。migrate:status には削除したファイルが表示されないので、ロールバックが効かない理由に気づきにくい状態です。ファイルは Git から戻してから操作します。

増えたマイグレーションをschema:dumpでまとめる手順

ファイルが数百本に増えたら、php artisan schema:dump で現在のスキーマを1つのSQLファイルに書き出せます。--prune を付けると既存のマイグレーションファイルを削除します。

php artisan schema:dump

# 書き出したうえで既存のマイグレーションを削除
php artisan schema:dump --prune

出力先は database/schema で、SQLite では sqlite-schema.sql という名前でした。このファイルにはテーブル定義に加えて INSERT INTO migrations VALUES(1,'0001_01_01_000000_create_users_table',1); のような実行記録も含まれます。まだ何も実行していないデータベースで migrate を実行すると、「Loading stored database schemas.」と表示してこのSQLを流し、記録済みのファイルは実行されず、ダンプ以降に追加したファイルだけが続けて実行されます。

対応するのは MariaDB・MySQL・PostgreSQL・SQLite で、各DBのコマンドラインクライアントを使って書き出します。SQLite なら sqlite3、MySQL なら mysqldump が実行環境に無いと使えません。テストで別の接続を使っているなら php artisan schema:dump --database=testing --prune のようにその接続でも書き出し、スキーマファイルはリポジトリにコミットします。

よくある質問

php artisan migrate:rollbackで1つだけ戻すにはどう書きますか?

php artisan migrate:rollback --step=1 と書きます。新しい順に1ファイルの down() だけが実行され、同じバッチの他のファイルは残ります。--step 1 と空白で区切っても同じ結果でした。値を付けない --step は最後のバッチ全体を戻すので注意してください。php artisan migrate --step=1 は別のコマンドで、値を取らないためエラーになります。

php artisan migrateのforceオプションは何のためにありますか?

APP_ENV が production のときに出る確認プロンプトを飛ばすオプションです。非対話の環境で付け忘れると「Command cancelled.」で終了コード1になるため、デプロイスクリプトでは必須です。失敗時のロールバックなど安全側の機能は何も足しません。本番で migrate:fresh などを確実に止めたい場合は、DB::prohibitDestructiveCommands() を併用します。

特定のマイグレーションファイルだけを実行できますか?

php artisan migrate --path=database/migrations/0001_01_01_000000_create_users_table.php のようにファイルを指定すると、そのファイルだけが実行されました。パスはアプリのベースパスからの相対で、絶対パスを渡すときは --realpath を併用します。--path は複数回指定でき、migrate:rollback や migrate:status にも同じオプションがあります。

Laravel 13でカラムを変更するのにdoctrine/dbalは必要ですか?

必要ありません。Laravel 11 のアップグレードガイドに「Laravel is no longer dependent on this package」とあり、Doctrine DBAL への依存は削除されています。13.31.0 の新規プロジェクトには doctrine/dbal が入っておらず、その状態で change() によるカラム変更が動きました。代わりに、残したい修飾子を change() の前にすべて書き直す必要があります。

DBマイグレーションとは何ですか?

データベースのテーブルやカラムの変更を、実行順と実行済みの記録を持つコードとして管理する方法です。Laravel では database/migrations のPHPファイルが変更内容、migrations テーブルが実行記録を担います。システムを別の基盤へ移す意味の「マイグレーション」とは別物で、Django の migrate や、DBマイグレーションツールの Flyway にも同じ考え方の仕組みがあります。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.08 テックブログ 大阪公立大学のランサムウェア被害と仮想化基盤の停止|全授業休講に至った経緯とバックアップを守る設定
  2. 2026.10.06 テックブログ アフラックの情報漏洩440万人|大量照会を止められなかった原因と照会量制御の実装
  3. 2026.10.07 テックブログ 旭化成ファーマのサイバー攻撃:Pharma DIGITAL会員51.4万人の漏えいと委託先DBの監視設計
  4. 2026.10.06 テックブログ 焼肉きんぐの不正アクセスと1,078万件の会員情報|全件規模の流出を防ぐAPIとログの点検
  5. 2026.10.06 テックブログ 大和証券の不正アクセスと約11万人分の口座番号:問い合わせ管理の委託先に残さない設計

RELATED POSTS 関連記事

目次