【実務完全ガイド】Laravelのマイグレーション・シーディング・Factory設計――安全なDB変更、テストデータ、初期データ、ロールバックまで
この記事で学べること(要点)
- Laravelのマイグレーションを安全に設計する考え方
- カラム追加・変更・削除で事故を減らす実務パターン
- SeederとFactoryを使った初期データ・テストデータの作り方
- 本番デプロイ時に気をつけたいDB変更の順序
- ロールバック、バックアップ、データ移行、ゼロダウンタイムを意識した運用
- 管理画面やフォームとつながる、分かりやすいデータ設計
- アクセシビリティを考慮した初期データ・文言・状態管理のポイント
想定読者
- Laravel 初〜中級エンジニア:マイグレーションは書けるが、本番DB変更が不安な方
- テックリード:チーム内でDB変更のルールやレビュー基準を整えたい方
- QA / 保守担当:テストデータや初期データが不安定で、検証しづらいと感じている方
- デザイナー / CS / アクセシビリティ担当:ステータス名、初期文言、画面表示に使うデータを分かりやすく整えたい方
アクセシビリティレベル:★★★★☆
この記事の主題はDB設計ですが、データの持ち方はUIの分かりやすさに直結します。たとえばステータス値、ロール名、通知テンプレート、フォーム選択肢が整理されていないと、画面側で説明しづらくなります。本記事では、色だけに頼らない状態表示、読みやすいラベル、翻訳しやすい初期データなども意識して解説します。
1. はじめに:マイグレーションは「DBの履歴書」です
Laravelのマイグレーションは、データベース構造をコードとして管理する仕組みです。テーブルを作る、カラムを追加する、インデックスを張る、外部キーを定義する、といった変更をファイルとして残せます。これは単なる便利機能ではなく、チーム開発や本番運用で非常に重要な役割を持ちます。
DB変更を手作業で行うと、「誰が、いつ、何を変えたか」が曖昧になります。ローカルでは動くのにステージングではカラムが無い、本番だけインデックスが足りない、テスト環境の初期データが違う、といった問題が起きやすくなります。マイグレーションを使うことで、DBの変更履歴をGitで管理でき、同じ手順をチーム全員が再現できるようになります。
ただし、マイグレーションは便利な反面、本番での扱いには注意が必要です。特にカラム削除、型変更、大量データ更新、外部キー追加、巨大テーブルへのインデックス追加は、サービス停止や長時間ロックの原因になることがあります。この記事では、Laravelのマイグレーションを「書き方」だけでなく、「安全に運用する考え方」として整理していきます。
2. 基本のマイグレーション:テーブル作成から始めます
まずは基本のテーブル作成です。たとえば記事テーブルを作る場合は、次のようにコマンドを実行します。
php artisan make:migration create_posts_table
生成されたファイルに、テーブル構造を書きます。
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', 120);
$table->string('slug')->unique();
$table->text('body');
$table->string('status', 30)->default('draft');
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->index(['status', 'published_at']);
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
この例で大切なのは、カラム名と状態が画面で使いやすい形になっていることです。
status には draft、published、archived のような値を入れる想定です。画面上では「下書き」「公開中」「アーカイブ済み」と翻訳・表示できます。DBには機械的に扱いやすい値を入れ、UIでは人が読みやすいラベルに変換する設計が扱いやすいです。
3. 命名ルール:後から読んでも分かる名前にします
マイグレーションファイル名は、後から履歴として読むことになります。
そのため、何をしたファイルか分かる名前にするのがおすすめです。
良い例:
create_posts_table
add_status_to_orders_table
add_published_at_to_posts_table
create_notification_preferences_table
分かりにくい例:
update_posts
fix_table
change_columns
add_fields
チーム開発では、マイグレーション名を見るだけで変更内容の概要が分かることが大切です。レビューでも、ファイル名が具体的だと内容を確認しやすくなります。
また、テーブル名やカラム名もUIや業務とつながるため、曖昧な名前は避けます。
たとえば flag より is_active、type より notification_type、value より display_order のように、意味が伝わる名前を選ぶと保守しやすくなります。
4. カラム追加:まずはnullableやdefaultを慎重に考えます
既存テーブルにカラムを追加する場合、本番データがすでに存在することを前提に考える必要があります。
たとえば users テーブルに timezone を追加する例です。
php artisan make:migration add_timezone_to_users_table
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('timezone', 64)->nullable()->after('email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('timezone');
});
}
};
最初は nullable() にしておくと、既存データがあっても安全に追加しやすいです。その後、アプリ側で未設定時のフォールバックを入れます。
$timezone = $user->timezone ?? config('app.timezone');
全ユーザーに値が入ったことを確認してから、必要であれば nullable を外す変更を検討します。
いきなり not null で追加すると、既存データに値が無くて失敗することがあります。DB変更は「追加」「移行」「制約強化」の順に分けると安全です。
5. カラム削除:本番では特に慎重に進めます
カラム削除は、DB変更の中でも危険度が高い操作です。理由は、一度削除するとアプリの旧コードが参照した瞬間にエラーになるからです。ゼロダウンタイムを目指す場合、いきなり削除せず段階を踏みます。
安全な流れの例:
- 新しいカラムを追加する
- アプリを新カラム対応にする
- 旧カラムを使わなくなる
- ログや監視で旧カラム参照がないことを確認する
- 別リリースで旧カラムを削除する
つまり、カラム削除は「最後の片付け」です。
削除マイグレーションは、機能変更と同じリリースに入れない方が安全なことが多いです。
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('old_summary');
});
このような操作は、レビュー時に「本当にアプリから参照されていないか」「バックアップはあるか」「ロールバック時に困らないか」を確認します。
6. インデックス設計:検索と一覧の速度を支えます
DBが遅くなる原因の多くは、インデックス不足や不適切なインデックスです。Laravelのマイグレーションでは、インデックスもコードで管理できます。
$table->index('status');
$table->index(['status', 'published_at']);
$table->unique('slug');
一覧画面でよく使う条件を考えて設計します。
たとえば、公開済み記事を公開日時順に表示するなら、次のような条件がよく出ます。
WHERE status = 'published'
ORDER BY published_at DESC
この場合、status と published_at の複合インデックスを検討できます。
$table->index(['status', 'published_at']);
ただし、インデックスは多ければよいわけではありません。書き込み時の負荷やストレージ使用量も増えます。
実務では、次の順番で考えると安全です。
- よく使う検索条件は何か
- よく使う並び順は何か
- 一覧画面の件数は多いか
- DBの実行計画でインデックスが使われているか
- 不要なインデックスが増えすぎていないか
インデックスは、画面の体感速度と運用コストの両方に影響します。
7. 外部キー制約:データ整合性をDBでも守ります
Laravelでは、外部キー制約も簡潔に書けます。
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
これは user_id が users.id を参照し、ユーザー削除時に関連データも削除する設定です。
ただし、cascadeOnDelete() は便利ですが、使いどころに注意が必要です。ユーザー削除と同時に投稿や注文が消える設計で本当に良いのか、業務要件に合わせて考えます。
たとえば注文データは、ユーザーを削除しても履歴として残したい場合があります。その場合は、外部キーの削除動作を変えるか、ユーザーを物理削除せずステータスで停止する設計にします。
$table->foreignId('user_id')
->nullable()
->constrained()
->nullOnDelete();
DB制約は、アプリ側のバリデーションとは別の最後の防衛線です。
アプリのバグや想定外操作があっても、DBが整合性を守ってくれる場面があります。
8. ステータス値の設計:UIで分かりやすく表示できるようにします
多くのアプリでは、status カラムを使います。
たとえば注文なら、次のような状態があります。
pendingpaidshippedcancelled
DBには英語の機械的な値を入れ、画面では翻訳して表示します。
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::Pending => '支払い待ち',
self::Paid => '支払い済み',
self::Shipped => '発送済み',
self::Cancelled => 'キャンセル済み',
};
}
}
LaravelのキャストでEnumを使えます。
protected function casts(): array
{
return [
'status' => OrderStatus::class,
];
}
状態表示では、色だけで意味を伝えないようにします。
たとえば赤いバッジだけでなく、「キャンセル済み」という文字を必ず表示します。これはアクセシビリティだけでなく、運用UIの誤解防止にも有効です。
9. Seeder:初期データをコードで再現します
Seederは、初期データを投入する仕組みです。
ロール、権限、カテゴリ、設定値、開発用データなどに使えます。
php artisan make:seeder RoleSeeder
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Role;
class RoleSeeder extends Seeder
{
public function run(): void
{
foreach (['admin', 'editor', 'viewer'] as $role) {
Role::firstOrCreate(['name' => $role]);
}
}
}
DatabaseSeeder から呼びます。
public function run(): void
{
$this->call([
RoleSeeder::class,
]);
}
Seederで大切なのは、何度実行しても壊れないことです。
create() だけを使うと、再実行時に重複することがあります。初期データには firstOrCreate() や updateOrCreate() を使うと安全です。
10. 本番Seederと開発Seederを分けます
開発用のダミーデータと、本番に必要な初期データは分けて考えます。
本番に必要な初期データの例:
- ロール
- 権限
- 都道府県
- 通知テンプレート
- システム設定
- 固定カテゴリ
開発用データの例:
- テストユーザー
- ダミー記事
- サンプル注文
- 大量のFactoryデータ
本番で開発用Seederを実行すると危険です。
Seeder名や呼び出し方を分けて、環境ごとに実行対象を明確にします。
public function run(): void
{
$this->call([
RoleSeeder::class,
PermissionSeeder::class,
]);
if ($this->app->environment('local')) {
$this->call([
DemoUserSeeder::class,
DemoPostSeeder::class,
]);
}
}
11. Factory:テストデータを読みやすく作ります
Factoryは、テストや開発でデータを作るための仕組みです。
php artisan make:factory PostFactory --model=Post
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->sentence(4),
'slug' => fake()->unique()->slug(),
'body' => fake()->paragraphs(3, true),
'status' => 'draft',
'published_at' => null,
];
}
public function published(): static
{
return $this->state(fn () => [
'status' => 'published',
'published_at' => now()->subDay(),
]);
}
}
使い方:
Post::factory()->published()->create();
Factoryに状態メソッドを用意すると、テストが読みやすくなります。
public function test_published_posts_are_visible()
{
Post::factory()->published()->create(['title' => '公開記事']);
Post::factory()->create(['title' => '下書き記事']);
$response = $this->get('/posts');
$response->assertSee('公開記事')
->assertDontSee('下書き記事');
}
テストデータは、単に数を増やすだけではなく、「意図が分かる状態」を作ることが大切です。
12. Factoryとリレーション:現実に近いデータを作ります
リレーションを含むテストデータも、Factoryで作れます。
$user = User::factory()
->has(Post::factory()->count(3)->published())
->create();
タグ付き投稿のような多対多関係も作れます。
$post = Post::factory()
->published()
->hasAttached(Tag::factory()->count(2))
->create();
ただし、テストで必要以上に大きなデータを作ると、テストが遅くなります。
重要なのは、現実に近い構造を小さく作ることです。
「100件必要」ではなく「この条件を満たす2件が必要」と考えると、テストは速く読みやすくなります。
13. データ移行:マイグレーション内で大量更新しすぎない
マイグレーションでカラムを追加すると同時に、既存データを更新したくなることがあります。
小さなテーブルなら問題ありませんが、大量データをマイグレーション内で一気に更新すると、本番デプロイが長時間止まる可能性があります。
危険な例:
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('timezone')->nullable();
});
User::query()->update(['timezone' => 'Asia/Tokyo']);
}
大量データの場合は、次のように分ける方が安全です。
- カラムを追加する
- アプリ側で未設定時のフォールバックを入れる
- バッチやジョブで少しずつデータを埋める
- 全件埋まったことを確認する
- 必要なら
nullableを外す
DB構造変更と大量データ更新は、同じマイグレーションに詰め込みすぎない方が安全です。
14. ロールバック:戻せる変更と戻せない変更を分けます
Laravelのマイグレーションには down() があります。ただし、本番では単純にロールバックできない変更もあります。
戻しやすい変更:
- 新規テーブル作成
- nullableカラム追加
- インデックス追加
戻しにくい変更:
- カラム削除
- 型変更
- データ変換
- 既存値の上書き
特にデータを失う変更は、down() を書いていても元に戻せないことがあります。
たとえばカラムを削除したあと、ロールバックでカラムを再作成しても、元のデータは戻りません。
そのため、ロールバックに頼りすぎず、次を準備します。
- デプロイ前バックアップ
- ステージングでの事前実行
- 小さな変更単位
- 互換性のあるリリース手順
- 必要なら手動復旧手順
down() は大切ですが、「本当に戻せるか」は別問題として考える必要があります。
15. 本番デプロイ時のDB変更:互換性を意識します
DB変更を含むデプロイでは、旧コードと新コードが一時的に混在する可能性があります。ゼロダウンタイムや複数台構成では特に重要です。
安全な原則は次の通りです。
- まず追加する
- 新旧どちらのコードでも動く状態にする
- 新コードへ切り替える
- 古い構造を後日削除する
例として、name を first_name と last_name に分ける場合、いきなり name を削除するのは危険です。
まず新カラムを追加し、アプリ側で両方扱えるようにし、データ移行し、完全に移行してから旧カラムを削除します。
このように、DB変更はコード変更よりも慎重に扱います。
テーブル構造はアプリ全体の土台なので、急な削除や変更は避けるのが安全です。
16. アクセシビリティとDB設計:状態・ラベル・選択肢を整える
DB設計は、画面の分かりやすさにも影響します。
たとえば、ステータス値が曖昧だと、UIでも曖昧な表示になります。
避けたい例:
status = 1
status = 2
status = 3
このような数値だけの状態は、あとから読みにくくなります。
もし使うなら、Enumや定数で意味を明確にします。
望ましい例:
draft
published
archived
画面では次のように表示できます。
- 下書き
- 公開中
- アーカイブ済み
色を付ける場合でも、文字ラベルがあるため、色覚に依存しません。
また、選択肢や初期データは翻訳しやすい構造にしておくと、多言語化にも対応しやすくなります。
17. テスト:マイグレーション、Seeder、Factoryの品質を守ります
Laravelでは、テスト時にマイグレーションを使ってDBを構築できます。
RefreshDatabase を使うと、テストごとにDB状態を整えやすくなります。
use Illuminate\Foundation\Testing\RefreshDatabase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_published_post_is_visible(): void
{
Post::factory()->published()->create([
'title' => '公開記事',
]);
$response = $this->get('/posts');
$response->assertSee('公開記事');
}
}
Seederのテストも重要です。
特にロールや権限のような初期データは、入っていないとアプリが動かないことがあります。
public function test_roles_are_seeded(): void
{
$this->seed(RoleSeeder::class);
$this->assertDatabaseHas('roles', [
'name' => 'admin',
]);
}
Factoryは、テストの読みやすさに直結します。
状態メソッドを増やしすぎる必要はありませんが、よく使う状態はFactoryへまとめておくと便利です。
18. よくある落とし穴と回避策
18.1 本番でいきなりnot nullカラムを追加する
既存データがあると失敗する可能性があります。
まず nullable で追加し、データ移行後に制約を強化します。
18.2 カラム削除を機能変更と同じリリースで行う
旧コードが参照するとエラーになります。
削除は後日、参照が無いことを確認してから行います。
18.3 Seederが重複データを作る
create() だけだと再実行で重複します。
firstOrCreate() や updateOrCreate() を使います。
18.4 Factoryが現実離れしすぎる
テストでしか成立しないデータになると、バグを見逃します。
最低限の整合性を持つFactoryにします。
18.5 ステータス値が数値だけで意味不明
画面表示や保守が難しくなります。
Enumや文字列値で意味を明確にします。
18.6 マイグレーションに大量データ更新を詰め込む
デプロイが長時間止まる可能性があります。
バッチやジョブで段階的に移行します。
19. チェックリスト(配布用)
マイグレーション設計
- [ ] ファイル名が変更内容を表している
- [ ] カラム名が意味を持っている
- [ ] 既存データがある前提で nullable / default を考えている
- [ ] カラム削除を慎重に分けている
- [ ] インデックスが検索条件に合っている
- [ ] 外部キー制約の削除動作を業務要件に合わせている
本番運用
- [ ] DB変更前にバックアップを確認している
- [ ] 大量データ更新をマイグレーションに詰め込んでいない
- [ ] 追加→移行→削除の順序を守っている
- [ ] ロールバックで本当に戻せるか確認している
Seeder
- [ ] 本番初期データと開発用データを分けている
- [ ]
firstOrCreate()/updateOrCreate()で再実行に強い - [ ] ロールや権限など必須データのSeederがある
Factory
- [ ] よく使う状態メソッドがある
- [ ] リレーション付きデータを自然に作れる
- [ ] テストに必要な最小件数で作っている
アクセシビリティ / UI連携
- [ ] ステータス値が画面で分かりやすく表示できる
- [ ] 色だけに依存しない状態ラベルを作れる
- [ ] 選択肢や初期データが翻訳しやすい
- [ ] 管理画面で意味のあるラベルに変換できる
20. まとめ
Laravelのマイグレーションは、DB構造を管理するだけでなく、チーム開発と本番運用を安定させるための重要な仕組みです。テーブル作成、カラム追加、インデックス、外部キーをコードで管理することで、環境差を減らし、変更履歴を明確にできます。
一方で、本番DB変更は慎重に扱う必要があります。特に、not null追加、カラム削除、型変更、大量データ移行は、追加→移行→削除のように段階を分けると安全です。SeederとFactoryを整えることで、初期データやテストデータも再現しやすくなります。
DB設計はバックエンドだけの話ではありません。ステータス値、ロール名、選択肢、初期文言は、画面の分かりやすさやアクセシビリティにもつながります。データを機械にも人にも扱いやすく整えることで、Laravelアプリ全体の品質が上がります。まずは、既存のマイグレーションとFactoryを見直し、意味が伝わる名前・安全な変更・再実行に強い初期データへ少しずつ整えていきましょう。
