11

介绍

伙计们,我有一个关于模型工厂和多个独特列的问题:

背景

我有一个名为 Image 的模型。该模型具有存储在单独模型ImageText中的语言支持。ImageText有一个image_id列、一个语言列和一个文本列。

ImageText在MySQL中有一个约束,即image_id和语言的组合必须是唯一的。

class CreateImageTextsTable extends Migration
{

    public function up()
    {
        Schema::create('image_texts', function ($table) {

            ...

            $table->unique(['image_id', 'language']);

            ...

        });
    }

    ...

现在,我希望每个Image在播种完成后都有几个ImageText模型。使用模型工厂和这个播种机很容易:

factory(App\Models\Image::class, 100)->create()->each(function ($image) {
    $max = rand(0, 10);
    for ($i = 0; $i < $max; $i++) {
        $image->imageTexts()->save(factory(App\Models\ImageText::class)->create());
    }
});

问题

但是,当使用模型工厂和 faker 播种时,您通常会收到以下消息:

[PDOException]                                                                                                                 
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '76-gn' for key 'image_texts_image_id_language_unique'

这是因为在某个时候,在 for 循环中,faker 会为一张图像随机使用相同的 languageCode 两次,从而打破了 ['image_id', 'language'] 的唯一约束。

你可以更新你的ImageTextFactory来这样说:

$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {

    return [
        'language' => $faker->unique()->languageCode,
        'title' => $faker->word,
        'text' => $faker->text,
    ];
});

但是,您会遇到一个问题,即在创建了足够的 imageTexts 之后,faker 将用完 languageCodes。

当前解决方案

目前通过为 ImageText 设置两个不同的工厂来解决这个问题,其中一个为 languageCodes 重置唯一计数器,播种器调用工厂,该工厂在进入 for 循环之前重置唯一计数器以创建更多 ImageText。但这是代码重复,应该有更好的方法来解决这个问题。

问题

有没有办法将您保存的模型发送到工厂?如果是这样,我可以在工厂内部检查一下当前图像是否已经附加了任何 ImageTexts,如果没有,则重置语言代码的唯一计数器。我的目标是这样的:

$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {

    $firstImageText = empty($image->imageTexts());

    return [
        'language' => $faker->unique($firstImageText)->languageCode,
        'title' => $faker->word,
        'text' => $faker->text,
    ];
});

当然,目前提供:

[ErrorException]           
Undefined variable: image

有可能以某种方式实现这一目标吗?

4

5 回答 5

9

我解决了

我搜索了很多解决此问题的方法,发现许多其他人也遇到过。如果您只需要关系另一端的一个元素,那非常简单

添加“多列唯一限制”使这变得复杂。我找到的唯一解决方案是“忘记 MySQL 的限制,只需使用针对 PDO 异常的 try-catch 来围绕工厂创建”。这感觉像是一个糟糕的解决方案,因为其他 PDOExceptions 也会被捕获,而且感觉不“正确”。

解决方案

为了完成这项工作,我将播种器分为 ImageTableSeeder 和 ImageTextTableSeeder,它们都非常简单。它们的运行命令都如下所示:

public function run()
{
    factory(App\Models\ImageText::class, 100)->create();
}

魔法发生在 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,
    ];
});

就是这个:

$imageIdAndLanguageCode = $faker->unique()->regexify("/^$imageId-[a-z]{2}");

我们在正则表达式中使用 imageId 并添加我们唯一组合中也包含的任何内容,在这种情况下用“-”字符分隔。这将生成“841-en”、“58-bz”、“96-xx”等结果,其中 imageId 始终是我们数据库中的真实图像,或者为空。

由于我们将 unique 标记与 imageId 一起粘贴到语言代码上,因此我们知道image_id 和 languageCode 的组合将是唯一的。这正是我们需要的!

现在我们可以简单地提取创建的语言代码,或者我们想要生成的任何其他唯一字段,使用:

$languageCode = explode('-', $imageIdAndLanguageCode)[1];

这种方法具有以下优点:

  • 无需捕获异常
  • 工厂和播种机可以分开以提高可读性
  • 代码紧凑

这里的缺点是您只能生成其中一个键可以表示为正则表达式的组合键。只要有可能,这似乎是解决这个问题的好方法。

于 2017-04-04T18:05:13.383 回答
2

我建立在 Rkey 的答案上以满足我的需求:

问题
我有两个整数字段,它们应该是唯一的,它们是product_idbranch_id

解决方案
这是我的方法:

  1. 获取产品和分支的总数。由于ids 是从 生成的1,因此ids 的范围应为1到表中的项目总数。
  2. product_id创建所有可能的唯一值,这些值可以通过创建由branch_id字符分隔的字符串来创建,在这种情况下-
  3. randomElements使用该函数从该集合中生成唯一的随机值。
  4. 将随机元素拆分回product_idbranch_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
    ];
于 2021-07-23T05:39:23.103 回答
1

您的解决方案仅适用于可以正则化为组合的事物。在许多用例中,多个单独的 Faker 生成的数字/字符串/其他对象的组合需要是唯一的并且不能被正则化。

对于这种情况,您可以执行以下操作:

$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];
});

对于您的特定问题/用例,以下是相同的解决方案:

$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,
    ];
});
于 2018-01-06T17:06:42.650 回答
0

我正在使用 Laravel 8.x,我不知道我使用的列函数定义是否适用于以前的版本。

我遇到了同样的问题并使用了不同的方法。

我这样创建ImageTextFactory

<?php

namespace Database\Factories;

use App\Models\ImageText;
use Illuminate\Database\Eloquent\Factories\Factory;

class ImageTextFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = ImageText::class;

    /**
     * The number of models created till now.
     *
     * @var integer
     */
    protected $created = 0;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        $this->created++;

        return [
            'language' => function (array $attributes) {
                $count = ImageText::where(
                    'image_id',
                    $attributes['image_id']
                )
                ->count();

                $reset = $this->created == 1 && $count == 0;

                return $this->faker->unique($reset)->languageCode();
            },
            'title' => $this->faker->word(),
            'text' => $this->faker->sentence(),
        ];
    }
}

然后我从播种机调用工厂:

Image::factory()
    ->count(10)
    ->has(
        ImageText::factory()->count(rand(0, 10))
    )->create();

使用定义中的函数,我可以检查是否先前定义ImageText了该函数image_id以及生成了多少模型。当ImageTextFactory为每个实例生成一个实例时,它会自动将计数器ImageFactory重置为 0;$created并且由于 Seeder 将始终按顺序创建图像,因此它一定不会产生问题。

它有一个缺点,如果为已经存在的模型调用工厂,它将OverflowException从 Faker 生成一个,因为没有新id的来重置唯一约束。它只能使用该has方法生成。

于 2021-07-27T04:30:27.697 回答
0

这是处理表播种器类中唯一约束问题的另一种方法。

我将以一个名为JobCategory的模型为例。

对于 JobCategory,列“title”具有唯一约束。

在工厂类中:

$factory->define(JobCategory::class, function (Faker $faker) {
    return [
        'title' => $faker->words(3, true),
        'description' => $faker->paragraphs(2, true),
    ];
});

然后,在播种机类中:

class JobCategoryTableSeeder extends Seeder
{
    private $failures = 0;

    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run() 
    {
        try {
            factory(JobCategory::class, 30)->create();
        } catch(Exception $e) {

            if($this->failures > 5) {
                print_r("Seeder Error. Failure count for current entity: " . $this->failures);
                return;
            }
            
            $this->failures++;
            $this->run(); // retry again until the number of failure is greater than 5
        }
    }
}

解释:

  • 这个想法是捕获可能由唯一约束失败导致的异常,然后通过递归调用该方法重试播种,直到满足退出条件。

  • 在上面的示例中,我想创建 30 条记录,但由于异常重试,我可能会得到多于或少于 30 条记录。

  • 我选择了 5 次重试,你可以使用任何合适的重试次数。

于 2020-08-08T19:58:07.637 回答