Laravel: Seeding multiple unique columns with Faker
I built on Rkey's answer to suit my needs:
problem
I have two integer fields that together should be unique, these are product_id
and branch_id
.
solution
Heres's my approach:
- Get the total number of products and branches. Since the
id
s are generated from1
, theid
s shall range from1
to the-total-count-of-items-in-the-table(s). - Create all possible unique values that can be created from
product_id
andbranch_id
by creating a string separated by a character, in this case-
- Generate unique random values from this set using the
randomElements
function. - Split the random element back to
product_id
andbranch_id
$branch_count = Branch::all()->count();
$product_count = Product::all()->count();
$branch_products = [];
for ($i = 1; $i <= $branch_count; $i++) {
for ($j = 1; $j <= $product_count; $j++) {
array_push($branch_products, $i . "-" . $j);
}
}
$branch_and_product = $this->faker->unique->randomElement($branch_products);
$branch_and_product = explode('-', $branch_and_product);
$branch_id = $branch_and_product[0];
$product_id = $branch_and_product[1];
return [
// other fields
// ...
"branch_id" => $branch_id,
"product_id" => $product_id
];
I solved it
I searched a lot for a solution to this problem and found that many others also experienced it. If you only need one element on the other end of your relation, it's very straight forward.
The addition of the "multi column unique restriction" is what made this complicated. The only solution I found was "Forget the MySQL restriction and just surround the factory creation with a try-catch for PDO-exceptions". This felt like a bad solution since other PDOExceptions would also get caught, and it just didn't feel "right".
Solution
To make this work I divided the seeders to ImageTableSeeder and ImageTextTableSeeder, and they are both very straight forward. Their run commands both look like this:
public function run()
{
factory(App\Models\ImageText::class, 100)->create();
}
The magic happens inside the ImageTextFactory:
$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {
// Pick an image to attach to
$image = App\Models\Image::inRandomOrder()->first();
$image instanceof App\Models\Image ? $imageId = $image->id : $imageId = null;
// Generate unique imageId-languageCode combination
$imageIdAndLanguageCode = $faker->unique()->regexify("/^$imageId-[a-z]{2}");
$languageCode = explode('-', $imageIdAndLanguageCode)[1];
return [
'image_id' => $imageId,
'language' => $languageCode,
'title' => $faker->word,
'text' => $faker->text,
];
});
This is it:
$imageIdAndLanguageCode = $faker->unique()->regexify("/^$imageId-[a-z]{2}");
We use the imageId in a regexify-expression and add whatever is also included in our unique combination, separated in this case with a '-' character. This will generate results like "841-en", "58-bz", "96-xx" etc. where the imageId is always a real image in our database, or null.
Since we stick the unique tag to the language code together with the imageId, we know that the combination of the image_id and the languageCode will be unique. This is exactly what we need!
Now we can simply extract the created language code, or whatever other unique field we wanted to generate, with:
$languageCode = explode('-', $imageIdAndLanguageCode)[1];
This approach has the following advantages:
- No need to catch exceptions
- Factories and Seeders can be separated for readability
- Code is compact
The disadvantage here is that you can only generate key combinations where one of the keys can be expressed as regex. As long as that's possible, this seems like a good approach to solving this problem.
Your solution only works for things that can be regexified as a combination. There are many use cases where a combination of multiple separate Faker generated numbers/strings/other objects need to be unique and cannot be regexified.
For such cases you can do something like so:
$factory->define(App\Models\YourModel::class, function (Faker\Generator $faker) {
static $combos;
$combos = $combos ?: [];
$faker1 = $faker->something();
while($faker2 = $faker->somethingElse() && in_array([$faker1, $faker2], $combos) {}
$combos[] = [$faker1, $faker2];
return ['field1' => $faker1, 'field2' => $faker2];
});
For your specific question / use case, here's a solution on the same lines:
$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {
static $combos;
$combos = $combos ?: [];
// Pick an image to attach to
$image = App\Models\Image::inRandomOrder()->first();
$image instanceof App\Models\Image ? $imageId = $image->id : $imageId = null;
// Generate unique imageId-languageCode combination
while($languageCode = $faker->languageCode && in_array([$imageId, $languageCode], $combos) {}
$combos[] = [$imageId, $languageCode];
return [
'image_id' => $imageId,
'language' => $languageCode,
'title' => $faker->word,
'text' => $faker->text,
];
});