404 Not Found

404 Not Found


nginx

Laravel データシーディングとモデルファクトリ

SeederとFactoryはLaravelの「データジェネレーター」です。たった1つのコマンドで数千件のリアルなテストデータを生成でき, 開発環境を瞬時に現実のシナリオに変身させることができます。

1. 学ぶ内容


2. テスターからの実話

(1) ペインポイント:手動データ生成はコードを書くより遅い

AliceがShopMetricsをテストするたび, テナントを手動で作成し, 店舗を追加し, 商品を入力し, 注文を発行しなければなりません。10件のテストデータを生成するのに30分かかります。Bobはさらにひどく, ページネーション機能をテストする必要がありましたが, 100件を手入力した後ブラウザがクラッシュしてすべてのデータを失いました。CharlieはSQLスクリプトを書いてデータを投入しようとしましたが, リレーションが複雑すぎました (テナント → 店舗 → 商品 → 注文 → 注文明細)。500行のSQLを書いても外部キーが1つ漏れていました。

(2) Factory + Seederのアプローチ

Laravel FactoryはFakerを使ってリアルなデータを自動生成し, Seederは投入順序を調整します。関連データはたった1行のコードで作成できます。

PHP
// ファクトリはリアルなデータを生成
$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) ファクトリの作成

BASH
php artisan make:factory ShopFactory
# モデルと一緒に作成
php artisan make:factory ShopFactory --model=Shop

(2) ファクトリの定義

PHP
// 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コアファクトリ定義

PHP
// 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']),
        ];
    }
}

出力:

TEXT
// 実行成功

4. ファクトリステート修飾子

(1) ステートの定義

PHP
// 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) ステートの使用

PHP
// アクティブな店舗 (デフォルト)
$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ファクトリステートの組み合わせ

PHP
// 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();

出力:

TEXT
// 実行成功

5. 関連データファクトリ

(1) リレーションの作成

PHP
// 商品付きの店舗を作成
$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データ統合ファクトリ

PHP
// 完全なテナントエコシステムを作成
$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();

出力:

TEXT
// 実行成功

6. Seederオーケストレーション

(1) Seederの作成

BASH
php artisan make:seeder TenantSeeder
php artisan make:seeder ProductSeeder

(2) Seederの記述

PHP
// 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) 呼び出しチェーン

PHP
// 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

PHP
// 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();
        });
    }
}

出力:

TEXT
// 実行成功

7. マルチテナントデータ分離投入

(1) テナント別データ割り当て

PHP
// 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) ▶ サンプル:投入ワークフローの実行

BASH
# フレッシュマイグレーション + シード (開発環境)
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

出力:

TEXT
# コマンド実行成功

8. 総合例:ShopMetricsの完全データ投入

PHP
// ============================================
// 総合: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]);
                    });
            });
        });
    }
}

❓ よくある質問

Q FactoryとSeederの違いは何ですか?
A Factoryは個別のデータレコードを生成するルールを定義します (金型のようなもの)。一方, Seederはデータ生成の順序と数量を調整します (組み立てラインのようなもの)。Factoryはテスト内で直接使用できますが, Seederはdb:seedコマンドで実行します。
Q make()create()の違いは何ですか?
A make()はモデルインスタンスを作成しますがDBには保存しません。永続化が不要な場面で使用します。create()はモデルインスタンスを作成してDBに保存します。IDとJOINクエリが必要な場面で使用します。
Q 日本語のテストデータを生成するにはどうすればよいですか?
A Faker言語をfake('ja_JP')->name()に変更するか, Factory内でfake()->locale('ja_JP')を使用してください。ただし, 国際化の観点からデータは英語のままにすることを推奨します。
Q Seederの実行が遅い場合はどうすればよいですか?
A Factoryの呼び出し回数を減らし, バッチ挿入に切り替えてください:DB::table('products')->insert($data)。モデルイベントを無効化:Product::withoutEvents(fn () => Product::factory()->count(1000)->create())
Q Seederは本番環境で使用できますか?
A はい, ただし注意が必要です。本番環境では初期データの追加 (plansテーブルなど)にのみSeederを使用し, Factoryでダミーデータを生成しないでください。--forceフラグを付ける際は必ずコマンドを確認してください。
Q テスト内でFactoryを使用するにはどうすればよいですか?
A テストメソッド内でFactoryを直接呼び出します:$shop = Shop::factory()->create()。Laravelは各テスト後にデータベーストランザクションを自動的にロールバックするため, テスト同士に影響はありません。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐):ShopMetricsのShopFactoryとProductFactoryを作成し, Tinkerを使って3つの店舗にそれぞれ5つの商品を生成し, 関連データが正しいことを確認してください。

  2. 応用問題 (⭐⭐):TenantIsolatedSeederを記述して5つのテナントを生成し, 各テナントに2つの店舗, 10の商品, 20の注文を含め, ファクトリ関連メソッド (has/for)を使用してデータを作成してください。

  3. チャレンジ (⭐⭐⭐):ステートを考慮した完全なデータシーディングソリューションを実装してください。ShopFactoryに3つのステート (active, suspended, highRevenue)を追加し, OrderFactoryに3つのステート (completed, cancelled, refunded)を追加し, Seederで割合分配してください (80% completed, 15% cancelled, 5% refunded)。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%