Environment
- Laravel 5.x
Laravel's factory is powerful — it lets you create hierarchical data with intuitive syntax. However, it issues one insert per record, so it needs a bit of care.
// Create 5 users
factory(User::class, 3)
->create()
->each(function (User $user) {
// Create 5 orders
factory(Order::class, 5)
->create(['user_id' => $user->id])
->each(function (Order $order) {
// Create 10 order_details
factory(OrderDetail::class, 10);
});
});
This code ends up issuing 3×5×10 = 150 inserts, which isn't great for performance. In cases like this, factory has a make function that only generates instances, which we can combine with insert.
// Create 3 users
$users = factory(User::class, 3)->make();
User::query()->insert($users->toArray());
// Find the users we just inserted
$users = User::query()->orderBy('created', 'desc')->limit(3)->get();
$users->each(function (User $user){
// Create 5 orders
$orders = factory(Order::class, 5)->make(['user_id' => $user->id]);
Order::query()->insert($orders->toArray());
// Find the orders we just inserted
$orders = Order::query()->orderBy('created', 'desc')->limit(5)->get();
$orders->each(function(Order $order){
// Create 10 order_details
$orderDetails = factory(OrderDetail::class, 10)->make([
'user_id' => $user->id, 'order_id' => $order->id
]);
OrderDetail::query()->insert($orderDetails->toArray());
});
});
It's frustrating that this makes the code harder to follow, but it reduced the number of SQL executions from 150 down to 23 (1+1+3*(2+5*(1))).
After I wrote the code above, I got a code review comment saying "this way would bring it down even further." With the code below, the total number of SQL executions comes down to 5.
<?php
// Create 3 users
$users = factory(User::class, 3)->make();
User::query()->insert($users->toArray());
$users = User::query()
->orderBy('created', 'desc')
->limit(3)
->get();
// Create the orders associated with the users
$orders = [];
$users->each(function (User $user){
$orders[] = factory(Order::class, 5)
->make(['user_id' => $user->id])
->toArray();
});
Order::query()->insert($orders);
// Fetch the orders we just created (linked to the users),
// then create the order_details linked to those orders
$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);
You can also use the make function when you want to insert data you've already prepared as an array, by passing it through a factory.
$vegetables = [
['vegetable_group' => 1, 'name' => 'daikon radish'],
['vegetable_group' => 1, 'name' => 'pumpkin'],
['vegetable_group' => 1, 'name' => 'sweet potato'],
['vegetable_group' => 2, 'name' => 'garlic chives'],
['vegetable_group' => 2, 'name' => 'lettuce'],
...
...
['vegetable_group' => 9, 'name' => 'enoki mushroom'],
['vegetable_group' => 9, 'name' => 'shimeji mushroom'],
];
$insertVegetables = [];
foreach($vegetables as $v){
$insertVegetables[] = factory(Vegetables::class)->make($v)->toArray();
}
Vegetables::query()->insert($insertVegetables);
Closing Thoughts
We had a project where the test suite was taking a long time to run, and wanting to improve that, I tuned it and wrote up what I did in this article. I'd be curious to know how larger projects design their test data.