R.Kumagaiをフォローする

【Laravel】リレーション関係のあるテストデータをfactoryで作成

バックエンド

はじめに

開発を行っていくと、どうしても必要になるテストデータですが、リレーション関係があるものなどを手動で作成しようと思うと、かなりの時間と体力を使う作業になるかと思います。
そこで今回は、私が過去に使用したfactoryを利用してリレーション関係があるデータを作成する方法をご紹介します。

前提情報

今回は、以下のテーブル関係で構成されたデータのテストデータを作成していきます。
※必要のないカラムも存在しますが、説明用に作成しております。

Modelは以下のような内容となっております。
基本的に、必要最低限のメソッドのみ定義しました。

Userモデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    public function userSkills()
    {
        return $this->hasMany(UserSkill::class);
    }
}

UserSkillモデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class UserSkill extends Model
{
    use HasFactory;

    public function user()
    {
        return $this->belongsTo(User::class);
    }
    public function skillDetails()
    {
        return $this->hasMany(SkillDetail::class);
    }
}

SkillDetailモデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class SkillDetail extends Model
{
    use HasFactory;

    public function userSkill()
    {
        return $this->belongsTo(UserSkill::class);
    }

}

factoryの内容

factoryファイルにはfakerを使用し、テスト用のダミーデータの作成ルールを記載しています。

まずは、UserFactoryです。

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * The current password being used by the factory.
     */
    protected static ?string $password;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // 1か2の数字をランダムで変数へ格納
        $gender = fake()->numberBetween(1, 2);
        // 性別のリスト
        $gender_list = [1 => '男性', 2 => '女性'];
        // 性別のリスト(名前用)
        $gender_name = [1 => 'male', 2 => 'female'];

        return [
            // ランダムにuuidを設定
            'uuid' => fake()->uuid(),
            // 性別に合わせたランダムな名前を設定
            'name' => fake()->name($gender_name[$gender]),
            // ランダムなメールアドレスを設定
            'email' => fake()->unique()->safeEmail(),
            // 現在の日時を設定
            'email_verified_at' => now(),
            // 'password'をハッシュ化して設定
            'password' => static::$password ??= Hash::make('password'),
            // ランダムな10文字の文字列を設定
            'remember_token' => Str::random(10),
            // '###-####'のフォーマットのランダムな数字を設定
            'zip_code' => fake()->numerify('###-####'),
            // ランダムな住所を設定
            'address' => fake()->address(),
            // ランダムに男性か女性を設定
            'gender' => $gender_list[$gender],
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

次にUserSkillFactoryです。

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
 */
class UserSkillFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // テスト用のスキル一覧
        $skill_list = [
            'php',
            'python',
            'java',
            'javaScript',
        ];
        // テスト用の経験年数合計一覧
        $experience_years_total_list = [
            '0.1',
            '1.5',
            '3',
            '3.5',
            '5',
            '5.5'
        ];
        return [
            // ランダムにスキル一覧から設定
            'skill_name' => fake()->randomElement($skill_list),
            // ランダムに経験年数合計一覧から設定
            'experience_years_total' => fake()->randomElement($experience_years_total_list),
        ];
    }
}

次にSkillDetailFactoryです。

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
 */
class SkillDetailFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // テスト用の経歴名一覧
        $career_name_list = [
            '開発案件',
            '自己学習',
            '学校',
        ];

        return [
            // 経歴名一覧からランダムな経歴を設定
            'career_name' => fake()->randomElement($career_name_list),
            // 1~9のランダムな数値を設定
            'using_years' => fake()->numberBetween(1, 9),
            // ランダムなテキストを設定
            'note' => fake()->realText(),
        ];
    }
}

これで、各モデルデータのデータ作成ルールを記載しました。
それでは次に、実際にデータ作成を行うSeederファイルの記載内容をご紹介します。

Seederの内容

Seederファイルは、基準となるUserのSeederのみ作成します。
作成するデータとしては、Userレコードを10レコード作成、それに紐づくUserSkillレコードを各3レコード、さらにそれに紐づくSkillDetailレコードを5レコード作成します。


UsersTableSeederの記載内容は以下のようになります。

<?php

namespace Database\Seeders;

use App\Models\SkillDetail;
use App\Models\User;
use App\Models\UserSkill;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Factories\Sequence;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // 基準となるUserを作成
        User::factory()
            ->has(
                // Userに紐づくUserSkillを3レコード作成
                UserSkill::factory()->count(3)
                    // Userのnameを紐づくUserSkillのuser_nameカラムへ挿入
                    ->state(function (array $attributes, User $user) {
                        return ['user_name' => $user->name];
                    })
                    // Userに紐づくUserSkillに紐づくSkillDetailを5レコード作成
                    ->has(SkillDetail::factory()->count(5)->state(new Sequence(
                        // 内3レコードは、固定のcareer_nameとusing_yearsを挿入
                        ['career_name' => '開発案件', 'using_years' => 1],
                        ['career_name' => '自己学習', 'using_years' => 3],
                        ['career_name' => '学校', 'using_years' => 5],
                        // UserSkillModelに記載されているリレーション関係を記載
                    )), 'skillDetails')
                    // UserModelに記載されているリレーション関係を記載
                , 'userSkills')->count(10)->create();
    }
}

上記を記載したら、DatabaseSeederへUsersTableSeederを実行するように記載します。

DatabaseSeeder

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            // 実行したいクラスを記載
            UsersTableSeeder::class,
        ]);
    }
}

DatabaseSeederへ記載しましたので、artisanコマンドから、Seederを実行します。
コマンドは、以下になります。

php artisan db:seed

上記を実行し、作成されたデータは以下になります。

usersテーブル

user_skillsテーブル

skill_detailsテーブル

終わりに

いかがだったでしょうか?
手作業でテストデータを作成するよりも何倍も楽かと思います。
プログラマーはなるべく楽をして業務の効率を上げていくと評価される職種であると思っておりますので、手作業でテストデータを作成されている方は参考にしてみてください。

おまけ

今回は、データの作成数を固定値にしましたが、ランダムで作成数を変更するパターンもご紹介させていただきます。
※コメントにも記載しておりますが、無制限にランダムにしてしまうと、とんでもない数になってしまう危険性があるため、今回は全て1~10の範囲内でランダムとしております。
変更ファイルは、UsersTableSeederのみになります。

UsersTableSeeder

<?php

namespace Database\Seeders;

use App\Models\SkillDetail;
use App\Models\User;
use App\Models\UserSkill;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Factories\Sequence;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // 無制限にランダムな数値だと、とんでもない数になってしまう可能性があるため
        // 今回は1~10までの数値内でランダムにしております。
        // Userの作成数 
        $user_count = fake()->numberBetween(1, 10);
        // UserSkillの作成数 
        $user_skill_count = fake()->numberBetween(1, 10);
        // SkillDetailの作成数 
        $skill_detail_count = fake()->numberBetween(1, 10);
        // 基準となるUserを作成
        User::factory()
            ->has(
                // Userに紐づくUserSkillを3レコード作成
                UserSkill::factory()->count($user_skill_count)
                    // Userのnameを紐づくUserSkillのuser_nameカラムへ挿入
                    ->state(function (array $attributes, User $user) {
                        return ['user_name' => $user->name];
                    })
                    // Userに紐づくUserSkillに紐づくSkillDetailを5レコード作成
                    ->has(SkillDetail::factory()->count($skill_detail_count)->state(new Sequence(
                        // 内3レコードは、固定のcareer_nameとusing_yearsを挿入
                        ['career_name' => '開発案件', 'using_years' => 1],
                        ['career_name' => '自己学習', 'using_years' => 3],
                        ['career_name' => '学校', 'using_years' => 5],
                        // UserSkillModelに記載されているリレーション関係を記載
                    )), 'skillDetails')
                    // UserModelに記載されているリレーション関係を記載
                , 'userSkills')->count($user_count)->create();
    }
}

上記を実行しますと、Userレコードを「1~10のランダムな数」レコード作成、それに紐づくUserSkillレコードを各「1~10のランダムな数」レコード、さらにそれに紐づくSkillDetailレコードを「1~10のランダムな数」レコード作成というデータが出来上がります。

ただ、上記ですと、一度ランダムな数値が決まったら固定されてしまうため、Userに紐づくデータ数は一律で同じ数となってしまいます。
Userに紐づくUserSkillのデータ数、そしてUserSkillに紐づくSkillDetailのデータ数もランダムにしたいという場合には、「UsersTableSeeder」「UserFactory」「UserSkillFactory」の3ファイルを変更する必要があります。
変更内容としては、SeederではUserテーブルのレコードのみ作成し、各factory内でリレーション関係のあるレコードを作成していくように変更していきます。

まずはUsersTableSeeder

<?php

namespace Database\Seeders;

use App\Models\SkillDetail;
use App\Models\User;
use App\Models\UserSkill;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Factories\Sequence;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // 無制限にランダムな数値だと、とんでもない数になってしまう可能性があるため
        // 今回は1~10までの数値内でランダムにしております。
        // Userの作成数 
        $user_count = fake()->numberBetween(1, 10);
        // 基準となるUserを作成
        User::factory()->count($user_count)->create();
    }
}

次にUserFactory

<?php

namespace Database\Factories;

use App\Models\User;
use App\Models\UserSkill;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * The current password being used by the factory.
     */
    protected static ?string $password;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // 1か2の数字をランダムで変数へ格納
        $gender = fake()->numberBetween(1, 2);
        // 性別のリスト
        $gender_list = [1 => '男性', 2 => '女性'];
        // 性別のリスト(名前用)
        $gender_name = [1 => 'male', 2 => 'female'];

        return [
            // ランダムにuuidを設定
            'uuid' => fake()->uuid(),
            // 性別に合わせたランダムな名前を設定
            'name' => fake()->name($gender_name[$gender]),
            // ランダムなメールアドレスを設定
            'email' => fake()->unique()->safeEmail(),
            // 現在の日時を設定
            'email_verified_at' => now(),
            // 'password'をハッシュ化して設定
            'password' => static::$password ??= Hash::make('password'),
            // ランダムな10文字の文字列を設定
            'remember_token' => Str::random(10),
            // '###-####'のフォーマットのランダムな数字を設定
            'zip_code' => fake()->numerify('###-####'),
            // ランダムな住所を設定
            'address' => fake()->address(),
            // ランダムに男性か女性を設定
            'gender' => $gender_list[$gender],
        ];
    }

    // リレーション関係のあるテーブルの作成処理
    public function configure()
    {
        // Userが作成された後の処理を記載
        return $this->afterCreating(function (User $user) {
            // 1から10のランダムな数のコメントを生成
            $user_skill_count = fake()->numberBetween(1, 10);
            // 作成されたUserに紐づくUserSkillを作成
            UserSkill::factory()->count($user_skill_count)->create(['user_id' => $user->id,'user_name' => $user->name]);
        });
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

次にUserSkillFactory

<?php

namespace Database\Factories;

use App\Models\SkillDetail;
use App\Models\UserSkill;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
 */
class UserSkillFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // テスト用のスキル一覧
        $skill_list = [
            'php',
            'python',
            'java',
            'javaScript',
        ];
        // テスト用の経験年数合計一覧
        $experience_years_total_list = [
            '0.1',
            '1.5',
            '3',
            '3.5',
            '5',
            '5.5'
        ];
        return [
            // ランダムにスキル一覧から設定
            'skill_name' => fake()->randomElement($skill_list),
            // ランダムに経験年数合計一覧から設定
            'experience_years_total' => fake()->randomElement($experience_years_total_list),
        ];
    }

    // リレーション関係のあるテーブルの作成処理
    public function configure()
    {
        // UserSkillが作成された後の処理を記載
        return $this->afterCreating(function (UserSkill $user_skill) {
            // 1から10のランダムな数のコメントを生成
            $skill_detail_count = fake()->numberBetween(1, 10);
            // 作成されたUserに紐づくSkillDetailを作成
            SkillDetail::factory()->count($skill_detail_count)->create(['user_skill_id' => $user_skill->id]);
        });
    }
}

上記を実行することで、紐づくテーブル数もランダムに作成することができます。