Laravel データシーディングとモデルファクトリ
SeederとFactoryはLaravelの「データジェネレーター」です。たった1つのコマンドで数千件のリアルなテストデータを生成でき, 開発環境を瞬時に現実のシナリオに変身させることができます。
1. 学ぶ内容
- モデルファクトリ定義:Fakerデータ生成とステート変更
- Seederコードと呼び出しチェーン:$this->call()
- 関連データファクトリ:ユーザー作成時に注文と商品を同時作成
- マルチテナントデータ分離:TenantSeederによるテナント単位のデータ投入
php artisan db:seedとmigrate:fresh --seedのワークフロー
2. テスターからの実話
(1) ペインポイント:手動データ生成はコードを書くより遅い
AliceがShopMetricsをテストするたび, テナントを手動で作成し, 店舗を追加し, 商品を入力し, 注文を発行しなければなりません。10件のテストデータを生成するのに30分かかります。Bobはさらにひどく, ページネーション機能をテストする必要がありましたが, 100件を手入力した後ブラウザがクラッシュしてすべてのデータを失いました。CharlieはSQLスクリプトを書いてデータを投入しようとしましたが, リレーションが複雑すぎました (テナント → 店舗 → 商品 → 注文 → 注文明細)。500行のSQLを書いても外部キーが1つ漏れていました。
(2) Factory + Seederのアプローチ
Laravel FactoryはFakerを使ってリアルなデータを自動生成し, Seederは投入順序を調整します。関連データはたった1行のコードで作成できます。
// ファクトリはリアルなデータを生成
$shop = Shop::factory()->create();
$shop->products()->createMany(
Product::factory()->count(10)->make()->toArray()
);
// 2行で1店舗 + 10商品
(3) 成果
Factoryを使用後, Aliceは30秒で1,000件のテストレコード (すべてのリレーションを含む)を生成し, ページネーションのテストにはmigrate:fresh --seedを実行するだけになりました。
3. モデルファクトリ
(1) ファクトリの作成
php artisan make:factory ShopFactory
# モデルと一緒に作成
php artisan make:factory ShopFactory --model=Shop
(2) ファクトリの定義
// database/factories/ShopFactory.php
class ShopFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'description' => fake()->paragraph(),
'status' => fake()->randomElement(['active', 'suspended', 'closed']),
'revenue' => fake()->randomFloat(2, 100, 100000),
];
}
}
(3) Fakerのよく使うメソッド
| メソッド | サンプル出力 | 説明 |
|---|---|---|
name() |
"Alice Johnson" | 名前 |
email() |
"alice@example.com" | メール |
company() |
"Acme Corp" | 会社名 |
slug() |
"acme-corp" | URLスラッグ |
paragraph() |
"Lorem ipsum..." | 段落テキスト |
randomFloat(2, 0, 100) |
45.67 | 小数 |
numberBetween(1, 100) |
42 | 整数範囲 |
randomElement([...]) |
配列からランダムに選択 | 列挙値 |
dateTimeThisYear() |
"June 15, 2024" | 日付 |
imageUrl() |
"https://via.placeholder.com/640x480" | 画像URL |
unique() |
一意性を保証 | 修飾子 |
(1) ▶ サンプル:ShopMetricsコアファクトリ定義
// database/factories/TenantFactory.php
class TenantFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'domain' => fake()->unique()->domainName(),
'status' => 'active',
];
}
}
// database/factories/ProductFactory.php
class ProductFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->words(3, true),
'sku' => strtoupper(fake()->unique()->lexify('???-????')),
'price' => fake()->randomFloat(2, 9.99, 999.99),
'stock' => fake()->numberBetween(0, 500),
'is_active' => true,
];
}
}
// database/factories/OrderFactory.php
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'order_number' => fake()->unique()->numerify('ORD-########'),
'subtotal' => fake()->randomFloat(2, 10, 5000),
'discount' => 0,
'total' => fake()->randomFloat(2, 10, 5000),
'status' => fake()->randomElement(['pending', 'processing', 'completed', 'cancelled']),
];
}
}
出力:
// 実行成功
4. ファクトリステート修飾子
(1) ステートの定義
// database/factories/ShopFactory.php
class ShopFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'status' => 'active',
'revenue' => fake()->randomFloat(2, 100, 50000),
];
}
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'suspended',
'revenue' => 0,
]);
}
public function highRevenue(): static
{
return $this->state(fn (array $attributes) => [
'revenue' => fake()->randomFloat(2, 50000, 500000),
]);
}
}
(2) ステートの使用
// アクティブな店舗 (デフォルト)
$shop = Shop::factory()->create();
// 停止中の店舗
$shop = Shop::factory()->suspended()->create();
// 高収益の店舗
$shop = Shop::factory()->highRevenue()->create();
// ステートの組み合わせ
$shop = Shop::factory()->highRevenue()->suspended()->create();
| 使用方法 | 説明 |
|---|---|
factory()->create() |
デフォルトステート, DBに保存 |
factory()->make() |
デフォルトステート, DBに保存しない |
factory()->suspended()->create() |
"suspended"ステートを使用 |
factory()->count(10)->create() |
10件作成 |
factory()->for(Tenant::factory()) |
親モデルに関連付け |
(1) ▶ サンプル:ShopMetricsファクトリステートの組み合わせ
// database/factories/OrderFactory.php
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'order_number' => fake()->unique()->numerify('ORD-########'),
'subtotal' => fake()->randomFloat(2, 10, 5000),
'discount' => 0,
'total' => fake()->randomFloat(2, 10, 5000),
'status' => 'pending',
];
}
public function completed(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'completed',
]);
}
public function cancelled(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'cancelled',
'total' => 0,
'subtotal' => 0,
]);
}
public function highValue(): static
{
return $this->state(fn (array $attributes) => [
'subtotal' => fake()->randomFloat(2, 1000, 10000),
'total' => fake()->randomFloat(2, 1000, 10000),
]);
}
}
// 使用例
$order = Order::factory()->completed()->highValue()->create();
出力:
// 実行成功
5. 関連データファクトリ
(1) リレーションの作成
// 商品付きの店舗を作成
$shop = Shop::factory()
->has(Product::factory()->count(10), 'products')
->create();
// またはマジックメソッドを使用
$shop = Shop::factory()
->hasProducts(10)
->create();
// 特定の店舗の注文を作成
$order = Order::factory()
->for($shop)
->create();
// テナント付きのユーザーを作成
$user = User::factory()
->for(Tenant::factory())
->create();
| メソッド | 説明 | 例 |
|---|---|---|
has() |
子関連を作成 | has(Product::factory()->count(5)) |
hasProducts(5) |
マジックメソッドの短縮形 | has(Product::factory()->count(5), 'products')と同等 |
for() |
親モデルに関連付け | for($tenant)またはfor(Tenant::factory()) |
(1) ▶ サンプル:ShopMetricsデータ統合ファクトリ
// 完全なテナントエコシステムを作成
$tenant = Tenant::factory()
->has(User::factory()->count(3))
->has(Shop::factory()->count(5)->hasProducts(10))
->create();
// 店舗の注文と明細を作成
$shop = Shop::factory()->create();
$products = Product::factory()->count(5)->for($shop)->create();
$order = Order::factory()
->for($shop->tenant)
->for($shop)
->for(User::factory()->for($shop->tenant))
->has(OrderItem::factory()->count(3)->state([
'product_id' => $products->random()->id,
]), 'items')
->create();
出力:
// 実行成功
6. Seederオーケストレーション
(1) Seederの作成
php artisan make:seeder TenantSeeder
php artisan make:seeder ProductSeeder
(2) Seederの記述
// database/seeders/TenantSeeder.php
class TenantSeeder extends Seeder
{
public function run(): void
{
Tenant::factory()
->count(10)
->has(User::factory()->count(3))
->has(Shop::factory()->count(5)->hasProducts(10))
->create();
}
}
(3) 呼び出しチェーン
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
PlanSeeder::class,
TenantSeeder::class,
ProductSeeder::class,
OrderSeeder::class,
]);
}
}
(1) ▶ サンプル:ShopMetrics完全DatabaseSeeder
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// 1. プランを作成
Plan::factory()->createMany([
['name' => 'Starter', 'slug' => 'starter', 'price' => 29, 'shop_limit' => 5],
['name' => 'Pro', 'slug' => 'pro', 'price' => 79, 'shop_limit' => 25],
['name' => 'Enterprise', 'slug' => 'enterprise', 'price' => 199, 'shop_limit' => null],
]);
// 2. 完全なデータ付きテナントを作成
Tenant::factory()
->count(20)
->has(User::factory()->count(5))
->has(Shop::factory()->count(3)->hasProducts(15))
->create();
// 3. 各店舗の注文を作成
Shop::all()->each(function ($shop) {
Order::factory()
->count(50)
->for($shop->tenant)
->for($shop)
->for($shop->tenant->users->random())
->create();
});
}
}
出力:
// 実行成功
7. マルチテナントデータ分離投入
(1) テナント別データ割り当て
// database/seeders/TenantIsolatedSeeder.php
class TenantIsolatedSeeder extends Seeder
{
public function run(): void
{
$tenants = Tenant::factory()->count(10)->create();
foreach ($tenants as $tenant) {
// 各テナントに分離されたデータを投入
$users = User::factory()->count(5)->for($tenant)->create();
$shops = Shop::factory()->count(3)->for($tenant)->create();
foreach ($shops as $shop) {
$products = Product::factory()->count(20)->for($shop)->create();
// 明細付きの注文を作成
Order::factory()->count(30)->for($tenant)->for($shop)
->for($users->random())
->create()
->each(function ($order) use ($products) {
$orderItems = $products->random(rand(1, 5));
foreach ($orderItems as $product) {
$order->items()->create([
'product_id' => $product->id,
'quantity' => rand(1, 3),
'price' => $product->price,
]);
}
$order->update([
'subtotal' => $order->items->sum(fn ($i) => $i->price * $i->quantity),
'total' => $order->items->sum(fn ($i) => $i->price * $i->quantity),
]);
});
}
}
}
}
(2) データ量の計画
| エンティティ | テナントあたり | 10テナント合計 | 備考 |
|---|---|---|---|
| Users | 5 | 50 | 1名はオーナー |
| Shops | 3 | 30 | すべてアクティブ |
| Products | 20/店舗 | 600 | ランダムにアクティブ/非アクティブ |
| Orders | 30/店舗 | 900 | 各種ステータス |
| OrderItems | 1-5/注文 | ~2,700 | ランダムな数量 |
(1) ▶ サンプル:投入ワークフローの実行
# フレッシュマイグレーション + シード (開発環境)
php artisan migrate:fresh --seed
# 特定のシーダーを実行
php artisan db:seed --class=TenantSeeder
# カスタム数量で実行
php artisan db:seed --class=OrderSeeder
# 本番環境 — migrate:freshは絶対に使わない!
php artisan migrate --force
php artisan db:seed --force
出力:
# コマンド実行成功
8. 総合例:ShopMetricsの完全データ投入
// ============================================
// 総合:ShopMetrics完全シーダー
// カバー:ファクトリ, ステート, リレーション, テナント分離
// ============================================
// database/seeders/ShopMetricsSeeder.php
class ShopMetricsSeeder extends Seeder
{
public function run(): void
{
// ステップ1:プラン
$starter = Plan::create(['name' => 'Starter', 'slug' => 'starter', 'price' => 29, 'shop_limit' => 5, 'features' => ['basic_analytics']]);
$pro = Plan::create(['name' => 'Pro', 'slug' => 'pro', 'price' => 79, 'shop_limit' => 25, 'features' => ['advanced_analytics', 'api_access']]);
$enterprise = Plan::create(['name' => 'Enterprise', 'slug' => 'enterprise', 'price' => 199, 'shop_limit' => null, 'features' => ['custom_analytics', 'api_access', 'sso']]);
// ステップ2:スーパー管理者を作成
User::factory()->create(['email' => 'admin@shopmetrics.io', 'role' => 'super_admin']);
// ステップ3:分離されたデータ付きテナントを作成
Tenant::factory()->count(20)->create()->each(function ($tenant) use ($starter, $pro, $enterprise) {
// ランダムなプランを割り当て
$plan = fake()->randomElement([$starter, $pro, $enterprise]);
Subscription::create(['tenant_id' => $tenant->id, 'plan_id' => $plan->id, 'status' => 'active']);
// テナントオーナーを作成
$owner = User::factory()->for($tenant)->create(['role' => 'tenant_owner']);
// アナリストを作成
$analysts = User::factory()->count(3)->for($tenant)->create(['role' => 'analyst']);
// 商品付きの店舗を作成
$shopCount = min(fake()->numberBetween(2, 5), $plan->shop_limit ?? 99);
Shop::factory()->count($shopCount)->for($tenant)->create()->each(function ($shop) use ($tenant, $owner, $analysts) {
// 店舗あたりの商品
Product::factory()->count(fake()->numberBetween(10, 30))->for($shop)->create();
// 店舗あたりの注文
Order::factory()->count(fake()->numberBetween(20, 80))
->for($tenant)
->for($shop)
->for(fake()->randomElement(array_merge([$owner], $analysts->all())))
->create()
->each(function ($order) use ($shop) {
$products = $shop->products()->inRandomOrder()->take(rand(1, 5))->get();
foreach ($products as $product) {
$qty = rand(1, 3);
$order->items()->create([
'product_id' => $product->id,
'quantity' => $qty,
'price' => $product->price,
]);
}
$subtotal = $order->items->sum(fn ($i) => $i->price * $i->quantity);
$order->update(['subtotal' => $subtotal, 'total' => $subtotal]);
});
});
});
}
}
❓ よくある質問
db:seedコマンドで実行します。make()とcreate()の違いは何ですか?make()はモデルインスタンスを作成しますがDBには保存しません。永続化が不要な場面で使用します。create()はモデルインスタンスを作成してDBに保存します。IDとJOINクエリが必要な場面で使用します。fake('ja_JP')->name()に変更するか, Factory内でfake()->locale('ja_JP')を使用してください。ただし, 国際化の観点からデータは英語のままにすることを推奨します。DB::table('products')->insert($data)。モデルイベントを無効化:Product::withoutEvents(fn () => Product::factory()->count(1000)->create())。plansテーブルなど)にのみSeederを使用し, Factoryでダミーデータを生成しないでください。--forceフラグを付ける際は必ずコマンドを確認してください。$shop = Shop::factory()->create()。Laravelは各テスト後にデータベーストランザクションを自動的にロールバックするため, テスト同士に影響はありません。📖 まとめ
- Factoryはデータ生成ルールを定義し, Fakerがリアルなランダムデータを提供します
- ステート変更 (state)は同じモデルに対して異なるデータシナリオを定義します
has()とfor()で関連データを作成し,hasProducts()マジックメソッドで構文を簡略化します- Seederは投入順序を整理し, DatabaseSeederが子Seederを呼び出します
- マルチテナントデータはテナントごとに投入し,
tenant_idが正しく関連付けられていることを確認します - 開発環境では
migrate:fresh --seedを使用し, 本番環境ではdb:seed --forceのみ使用します
📝 練習問題
-
基本問題 (⭐):ShopMetricsのShopFactoryとProductFactoryを作成し, Tinkerを使って3つの店舗にそれぞれ5つの商品を生成し, 関連データが正しいことを確認してください。
-
応用問題 (⭐⭐):
TenantIsolatedSeederを記述して5つのテナントを生成し, 各テナントに2つの店舗, 10の商品, 20の注文を含め, ファクトリ関連メソッド (has/for)を使用してデータを作成してください。 -
チャレンジ (⭐⭐⭐):ステートを考慮した完全なデータシーディングソリューションを実装してください。
ShopFactoryに3つのステート (active, suspended, highRevenue)を追加し,OrderFactoryに3つのステート (completed, cancelled, refunded)を追加し, Seederで割合分配してください (80% completed, 15% cancelled, 5% refunded)。



