【问题标题】:Laravel: Seeding multiple unique columns with FakerLaravel:使用 Faker 播种多个唯一列
【发布时间】:2017-08-29 09:32:06
【问题描述】:

简介

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

背景

我有一个名为 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

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

【问题讨论】:

    标签: php mysql laravel laravel-5.4 laravel-seeding


    【解决方案1】:

    我使用的是 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 以及生成了多少模型。当为每个ImageFactory 生成一个ImageTextFactory 实例时,它会自动将$created 计数器重置为0;并且由于 Seeder 将始终按顺序创建图像,因此它一定不会产生问题。

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

    【讨论】:

    • 通过 Laravel 5.4 文档做了一点研究,发现支持列函数定义。 link
    【解决方案2】:

    我根据 Rkey 的回答来满足我的需求:

    问题
    我有两个整数字段,它们应该是唯一的,它们是 product_id 和 branch_id。

    解决方案
    这是我的方法:

    1. 获取产品和分支的总数。由于ids 是从1 生成的,因此ids 的范围应从1 到the-total-count-of-items-in-the-table(s)。
    2. 通过创建由字符分隔的字符串(在本例中为 -),创建可以从 product_id 和 branch_id 创建的所有可能的唯一值
    3. 使用randomElements 函数从这个集合中生成唯一的随机值。
    4. 将随机元素拆分回product_id和branch_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
        ];
    

    【讨论】:

      【解决方案3】:

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

      我将以一个名为 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 次重试,你可以使用任意合适的重试次数。

      【讨论】:

        【解决方案4】:

        您的解决方案仅适用于可以正则化为组合的事物。在许多用例中,多个独立的 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,
            ];
        });
        

        【讨论】:

        • 这个解决方案很棒。我在三列上有一个唯一索引。我在 while 子句中生成了三个值。我必须将每个变量赋值括在括号中才能设置它们。
        【解决方案5】:

        我解决了

        我搜索了很多解决此问题的方法,发现许多其他人也遇到过。如果您只需要关系另一端的一个元素,it's very straight forward。

        “多列唯一限制”的添加使这变得复杂。我找到的唯一解决方案是“忘记 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 始终是我们数据库中的真实图像,或者为 null。

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

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

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

        这种方法有以下优点:

        • 无需捕获异常
        • 为了便于阅读,工厂和播种机可以分开
        • 代码紧凑

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-09-03
          • 2018-09-10
          • 2023-03-03
          • 2019-05-18
          • 2015-06-02
          • 2018-05-26
          • 2018-11-11
          • 2022-07-21
          相关资源
          最近更新 更多