環境
  • Laravel 5系

Laravelのfactoryは強力で、階層構造を持ったデータも直感的な記述で作成することができます。ですが、1件ずつinsertが発行されてしまう為、少し注意が必要です。

// usersを5件つくる
factory(User::class, 3)
->create()
->each(function (User $user) {
    // ordersを5件つくる
    factory(Order::class, 5)
    ->create(['user_id' => $user->id])
    ->each(function (Order $order) {
        // order_detailsを10件つくる
        factory(OrderDetail::class, 10);
    });
});

このコードでは、3×5×10で150回のinsertが作成されてしまい、パフォーマンス的にあまりよろしくありません。
このような場合、factoryにはinstanceのみ生成するmakeという関数があるので、これをinsertと合わせて使います。

// usersを3件追加
$users = factory(User::class, 3)->make();
User::query()->insert($users->toArray());
//insertしたuserをfind
$users = User::query()->orderBy('created', 'desc')->limit(3)->get(); 
$users->each(function (User $user){

    // ordersを5件作成
    $orders = factory(Order::class, 5)->make(['user_id' => $user->id]);
    Order::query()->insert($orders->toArray());
    //insertしたorderをfind
    $orders = Order::query()->orderBy('created', 'desc')->limit(5)->get(); 

    $orders->each(function(Order $order){
        // order_detailsを10件作成
        $orderDetails = factory(OrderDetail::class, 10)->make([
            'user_id' => $user->id, 'order_id' => $order->id
        ]);
        OrderDetail::query()->insert($orderDetails->toArray());
    });
});

コードの見通しが悪くなるのが悔しいですが、SQLの実行回数は150回から、23回(1+1+3*(2+5*(1)))まで減らすことができました。

上のコードを書いた所、「この方が少なくなるよ」とレビューをいただきました。以下のコードでは、SQLの実行回数は合計5回になります。

<?php 

// Userを3件作成
$users = factory(User::class, 3)->make();
User::query()->insert($users->toArray());
$users = User::query()
    ->orderBy('created', 'desc')
    ->limit(3)
    ->get(); 

// Userに紐づくOrdersを作成
$orders = [];
$users->each(function (User $user){
    $orders[] = factory(Order::class, 5)
        ->make(['user_id' => $user->id])
        ->toArray();
});
Order::query()->insert($orders);

// 作成した、Userに紐づくOrderを取得をして、
// そのOrderに紐づくOrderDetailを作成
$orderDetails = []
$orders = Order::query()
    ->whereIn('user_id', $users->pluck('user_id')
    ->toArray())
    ->get();
$orders->each(function(Order $order){
    $orderDetails[] = factory(OrderDetail::class, 10)
        ->make(['user_id' => $user->id, 'order_id' => $order->id])
        ->toArray());
});
OrderDetail::query()->insert($orderDetails);

また、make関数は、配列で準備しているデータをfacotryを通してinsertしたい場合にも使うことができます。

$vegetables = [
    ['vegetable_group' => 1, 'name' => '大根'],
    ['vegetable_group' => 1, 'name' => 'かぼちゃ'],
    ['vegetable_group' => 1, 'name' => 'さつまいも'],
    ['vegetable_group' => 2, 'name' => 'にら'],
    ['vegetable_group' => 2, 'name' => 'レタス'],
    ...
    ...
    ['vegetable_group' => 9, 'name' => 'えのき'],
    ['vegetable_group' => 9, 'name' => 'しめじ'],
];

$insertVegetables = [];
foreach($vegetables as $v){
    $insertVegetables[] = factory(Vegetables::class)->make($v)->toArray();
}
Vegetables::query()->insert($insertVegetables);

さいごに

テストの実行に時間がかかっているプロジェクトがあり、改善したい思いチューニングした内容を記事にしました。大規模なプロジェクトはどんな風にテストデータ設計しているか知りたいなと思いました。