web-dev-qa-db-ja.com

Laravel:Fakerで複数の一意の列をシードする

はじめに

どうしたのか、モデルファクトリと複数の一意の列について質問がありました。

背景

Imageという名前のモデルがあります。このモデルには、別のモデルImageTextに格納されている言語サポートがあります。 ImageTextにはimage_id列、言語列、テキスト列があります。

ImageTextMySQLには、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());
    }
});

問題

ただし、モデルファクトリとフェイカーを使用してこれをシードすると、次のメッセージが表示されることがよくあります。

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

これは、ある時点で、そのforループ内で、偽造者が画像に対して同じlanguageCodeを2回ランダム化し、['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が作成された後、偽造者がlanguageCodesを使い果たすという問題が発生します。

現在の解決策

これは現在、ImageTextに2つの異なるファクトリを用意することで解決されています。一方は、languageCodesの一意のカウンターをリセットし、シーダーは、forループに入る前に一意のカウンターをリセットするファクトリを呼び出してさらにImageTextを作成します。しかし、これはコードの重複であり、これを解決するためのより良い方法があるはずです。

質問

保存しているモデルを工場に送る方法はありますか?もしそうなら、私は現在の画像にすでにImageTextが添付されているかどうかを確認するために工場内でチェックし、そうでない場合は、languageCodesの一意のカウンターをリセットすることができます。私の目標は次のようなものです。

$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

どういうわけかこれを達成することは可能ですか?

7
Rkey

私はそれを解決しました

私はこの問題の解決策をたくさん探しましたが、他の多くの人もそれを経験していることがわかりました。リレーションのもう一方の端に要素が1つだけ必要な場合は、 非常に簡単です

「複数列の一意の制限」の追加が、これを複雑にした理由です。私が見つけた唯一の解決策は、「MySQLの制限を忘れて、工場での作成をPDO例外のtry-catchで囲む」というものでした。他のPDOExceptionもキャッチされるため、これは悪い解決策のように感じられ、「正しい」とは感じられませんでした。

ソリューション

この作業を行うために、シーダーを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}");

Regexify-expressionでimageIdを使用し、一意の組み合わせに含まれているものをすべて追加します。この場合は「-」文字で区切ります。これにより、「841-en」、「58-bz」、「96-xx」などの結果が生成されます。ここで、imageIdは常にデータベース内の実際の画像、またはnullです。

一意のタグをimageIdと一緒に言語コードに貼り付けるので、image_idとlanguageCodeの組み合わせが一意になることがわかります。これはまさに私たちが必要としているものです!

これで、作成した言語コード、または生成したいその他の一意のフィールドを次の方法で簡単に抽出できます。

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

このアプローチには、次の利点があります。

  • 例外をキャッチする必要はありません
  • 工場とシーダーは読みやすくするために分離できます
  • コードはコンパクトです

ここでの欠点は、キーの1つを正規表現として表現できるキーの組み合わせしか生成できないことです。それが可能である限り、これはこの問題を解決するための良いアプローチのようです。

6
Rkey

あなたの解決策は、組み合わせとして再登録できるものに対してのみ機能します。複数の個別の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,
    ];
});
1
Paras